Androidアプリ開発

Licenses Gradle PluginのWebView対応

この記事は約42分で読めます。
記事内に広告が含まれています。
スポンサーリンク

この記事はAndroidスマホ用のアプリ開発の中で、
今後の開発で再使用性が高いと思われるコーディングをまとめたものです。
Javaの開発経験、XML構文規則、Androidのアプリ開発経験がある方を対象としています。
Androidのアプリ開発でお役にたててれば、嬉しいです。
(これからAndroidのアプリ開発やJavaでの開発を始めたい方への案内は、
記事の最後で紹介します)

この記事のテーマ


Licenses Gradle Pluginを使ってOSSライセンスをWebViewに表示する

ポイント

オープンソースライセンス管理のlicense-tools-plugin(クックパッド社)はGradle-9で動作しなくなりました。
今回はLicenses Gradle Pluginを使用してHTMLを生成、OSSライセンス情報をWebViewに表示する方法を紹介します。

オープンソースの通知を含める

OSSライセンス情報を取得する

プロジェクトで使用しているオープンソースのライセンス情報をLicenses Gradle Pluginで取得します。

Licenses Gradle Pluginの導入

build.gradle(Project)にプラグインの使用を追記します。

build.gradle(Project)

    dependencies {
        classpath libs.oss.licenses.plugin
        :   
    }

build.gradle(Module)

plugins {
    :
    id 'com.google.android.gms.oss-licenses-plugin'
}
:

dependencies {
    :
    implementation libs.play.services.oss.licenses
}

gradle/libs.versions.toml

[versions]
ossLicensesPlugin = "0.13.0"
playServicesOssLicenses = "17.5.2"

[libraries]
oss-licenses-plugin = { module = "com.google.android.gms:oss-licenses-plugin", version.ref = "ossLicensesPlugin" }
play-services-oss-licenses = { module = "com.google.android.gms:play-services-oss-licenses", version.ref = "playServicesOssLicenses" }
Licenses Gradle Pluginの実行

Android Studioのターミナルから、リリースビルドを実行します。

>./gradlew clean assembleRelease

プロジェクトのビルドフォルダ配下にOSSライセンス情報(3種類)が出力されます。
出力されたOSSライセンス情報はプロジェクトのassetsフォルダ(要作成)にコピーします。

dependencies.json (出力先…\app\build\generated\third_party_licenses\release)
third_party_license_metadata (出力先…\app\build\generated\res\releaseOssLicensesTask\raw)
third_party_licenses (出力先…\app\build\generated\res\releaseOssLicensesTask\raw)

OSSライセンス情報からHTMLを生成する

OSSライセンス情報にはライブラリ名とライセンス本文が含まれています。
生成するHTMLに著作権者、リンク先を追加するためにYAMLを使用します。
assetsフォルダにコピーにしたOSSライセンス情報とYAMLから、HTMLを生成する仕組みを実装します。

OSSライセンス情報から生成したYMAL

artifact: :MPAndroidChart:+
name: MPAndroidChart
copyrightHolder: #COPYRIGHT_HOLDER#
url: #URL#

編集後のYAML

artifact: com.github.PhilJay:MPAndroidChart:+
name: MPAndroidChart
copyrightHolder: 2020 Philipp Jahoda
url:
https://github.com/philjay/mpandroidchart

ライブラリのパッケージ、copyrightHolder、リンクを追記します。

ライセンス情報の管理

HTML生成に必要なデータ(ライセンス情報)を管理するテーブル(entity)をデータアクセス(dao)を用意します。

License

@Entity(tableName = "License", indices = {@Index(value = "name", unique = true)})
public class License {
    @PrimaryKey(autoGenerate = true)
    public Integer seq;
    @ColumnInfo(name = "group")
    public String group;
    @ColumnInfo(name = "name")
    public String name;
    @ColumnInfo(name = "version")
    public String version;
    @ColumnInfo(name = "copyright")
    public String copyright;
    @ColumnInfo(name = "license")
    public String license;
    @ColumnInfo(name = "url")
    public String url;
    public License(Integer seq, String group, String name, String version, String copyright, String license, String url) {
        this.seq = seq;
        this.group = group;
        this.name = name;
        this.version = version;
        this.copyright = copyright;
        this.license = license;
        this.url = url;
    }
}

LicenseDao

public interface LicenseDao {
    // 参照
    @Query("SELECT * FROM License ORDER BY seq ASC")
    List<License> select();
    @Query("SELECT * FROM License WHERE name = :name ORDER BY seq DESC LIMIT 1")
    License name(String name);
    @Query("SELECT * FROM License WHERE license = :license AND copyright = '' ORDER BY seq ASC")
    List<License> licenses(String license);
    // 追加
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    void insert(License license);
    // 更新
    @Update(onConflict = OnConflictStrategy.REPLACE)
    void update(License license);
    // 削除
    @Delete
    void delete(License license);
}
ライセンス本文のファイル出力

assetsフォルダのthird_party_license_metadataからthird_party_licensesのライセンスをファイル出力します。third_party_license_metadataにある開始位置と長さでthird_party_licensesを分割してファイル出力します。
ライセンスにはライセンス本文とURLの2種類あります。
ファイル出力後、ライセンス情報からHTMLを生成します。

    private Context             context;
    private LicenseDao          licenseDao;
    private File                file;
    private ArrayList<License>  arrayLicense = new ArrayList<>();
    private ArrayList<String>   arrayString = new ArrayList<>();
    private static final String HTML = "licenses.html";
    private static final String YAML = "licenses.yml";
    private static final String TPL = "third_party_licenses";
    private static final String TPLM = "third_party_license_metadata";
    :
    public void step1(Context context) {
        this.context = context;
        AssetManager assetManager = context.getAssets();
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.execute(() -> {
            try {
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(assetManager.open(TPLM)));
                String string;
                arrayLicense = new ArrayList<>();
                while ((string = bufferedReader.readLine()) != null) {
                    arrayLicense.add(new License(null, "", string.substring(string.indexOf(" ") + 1).replace(" ", "-").toLowerCase(), "", "", string.substring(0, string.indexOf(" ")).replace(":", "_"), ""));
                }
                try {
                    InputStream inputStream = assetManager.open(TPL);
                    bytes = ByteStreams.toByteArray(inputStream);
                    inputStream.close();
                } catch (IOException e) {
                    Log.d(TAG, Objects.requireNonNull(e.getMessage()));
                }
                for (License license : arrayLicense) {
                    if (!setFile(license.license)) {
                        int off = Integer.parseInt(license.license.substring(0, license.license.indexOf("_")));
                        int len = Integer.parseInt(license.license.substring(license.license.indexOf("_") + 1));
                        try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
                            fileOutputStream.write(bytes, off, len);
                            fileOutputStream.flush();
                        } catch (Exception e) {
                            Log.d(TAG, Objects.requireNonNull(e.getMessage()));
                        }
                        ArrayList<String> arrayText = readFileString(file);
                        if (arrayText.size() == 1 && arrayText.get(0).toLowerCase().contains("http")) {
                            // URLの場合はHTML化したライセンス本文をファイル出力
                            :
                        } else {
                            // ライセンス本文テキストをファイル出力
                            initializeFile(license.license);
                            boolean p = false;
                            boolean w = false;
                            for (String text : arrayText) {
                                if (text.isEmpty()) {
                                    writeFile(p? "</p>\n" : "<p\n>", true);
                                    p = !p;
                                    w = true;
                                } else {
                                    if (!w) {
                                        writeFile("<p>\n", true);
                                        p = true;
                                        w = true;
                                    }
                                    writeFile(String.format("%s\n", text.replace("&", "&").replace("<", "<").replace(">", ">")), true);
                                }
                            }
                            if (p) writeFile("</p>\n", true);
                        }
                    }
                }
                bufferedReader = new BufferedReader(new InputStreamReader(assetManager.open(YAML)));
                arrayString = new ArrayList<>();
                while ((string = bufferedReader.readLine()) != null) {
                    arrayString.add(string);
                }
            } catch (Exception e) {
                Log.d(TAG, Objects.requireNonNull(e.getMessage()));
            }
            new Handler(Looper.getMainLooper()).post(() -> stemp2(context, arrayLicense, arrayString));
        });
    }
    :

    public boolean initializeFile(String fileName) {
        file = new File(context.getFilesDir(), fileName);
        return (!file.exists() || file.delete());
    }

    public ArrayList<String> readFileString(File file) {
        ArrayList<String> stringList = new ArrayList<>();
        try (FileInputStream fileInputStream = new FileInputStream(file);
             InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8);
             BufferedReader reader= new BufferedReader(inputStreamReader) ) {
            String lineBuffer;
            while( (lineBuffer = reader.readLine()) != null ) {
                stringList.add(lineBuffer);
            }
        } catch (Exception e) {
            Log.d(TAG, Objects.requireNonNull(e.getMessage()));
        }
        return stringList;
    }

    public void writeFile(String data, boolean mode) {
        try (FileOutputStream fileOutputStream = new FileOutputStream(file, mode);
            OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8);
            BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter)) {
            bufferedWriter.write(data);
            bufferedWriter.flush();
        } catch (Exception e) {
            Log.d(TAG, Objects.requireNonNull(e.getMessage()));
        }
    }
ライセンス情報からHTMLを生成する

ライセンス本文のファイル出力後、ライセンス情報を管理テーブルを更新します。
更新後の管理テーブルから、YAMLとHTMLを内部ストレージに出力します。
HTMLの生成ではライセンス本文のHTMLをマージします。

    private Context             context;
    private LicenseDao          licenseDao;
    private File                file;
    private ArrayList<License>  arrayLicense = new ArrayList<>();
    private ArrayList<String>   arrayString = new ArrayList<>();
    private static final String HTML = "licenses.html";
    private static final String YAML = "licenses.yml";
    private static final String TPL = "third_party_licenses";
    private static final String TPLM = "third_party_license_metadata";
    private static String HEADER ="<!DOCTYPE html>\n" +
            "<html>\n" +
            "<head>\n" +
            "  <meta charset=\"UTF-8\"/>\n" +
            "  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n" +
            "  <style>\n" +
            "    body {\n" +
            "    color: #4c4a40;\n" +
            "    font-size: 87.5%;\n" +
            "    }\n" +
            "    .header, .library {\n" +
            "    margin: 1em 0;\n" +
            "    padding: 0 0.5em;\n" +
            "    border-bottom: 1px solid #ebeae6;\n" +
            "    }\n" +
            "    .title {\n" +
            "    font-size: 129%;\n" +
            "    font-weight: bold;\n" +
            "    margin: 1em 0\n" +
            "    }\n" +
            "    .license {\n" +
            "    background-color: #f7f7f9;\n" +
            "    border-radius: 4px;\n" +
            "    border: 1px solid #e1e1e8;\n" +
            "    font-size: 80%;\n" +
            "    padding: 9px 14px;\n" +
            "    margin-bottom: 14px;\n" +
            "    }\n" +
            "    .license h1, .license h2 {\n" +
            "    font-size: 100%;\n" +
            "    font-weight: bold;\n" +
            "    }\n" +
            "    .license .inline {\n" +
            "    display: inline;\n" +
            "    }\n" +
            "    .license .block {\n" +
            "    margin: 1em 0;\n" +
            "    }\n" +
            "    .license pre {\n" +
            "    white-space: pre-wrap;\n" +
            "    }\n" +
            "    .license .low-alpha {\n" +
            "    list-style-type: lower-alpha;\n" +
            "    padding-left: 2em;\n" +
            "    }\n" +
            "    input ~ .license {\n" +
            "    max-height: 200px;\n" +
            "    overflow: hidden;\n" +
            "    }\n" +
            "    input, label {\n" +
            "    position: absolute;\n" +
            "    right: 16px;\n" +
            "    width: 12px;\n" +
            "    height: 12px;\n" +
            "    margin: 20px;\n" +
            "    }\n" +
            "    input {\n" +
            "    opacity: 0;\n" +
            "    z-index: 1;\n" +
            "    }\n" +
            "    label {\n" +
            "    margin-top: 16px;\n" +
            "    text-align: center;\n" +
            "    font-weight: bold;\n" +
            "    }\n" +
            "    label::before {\n" +
            "    content: \"+\";\n" +
            "    }\n" +
            "    input:checked + label::before {\n" +
            "    content: \"-\";\n" +
            "    }\n" +
            "    input:checked ~ .license {\n" +
            "    max-height: none;\n" +
            "    }\n" +
            "    a {\n" +
            "    overflow-wrap: break-word;\n" +
            "    }\n" +
            "  </style>\n" +
            "</head>\n" +
            "<body>\n";
    private static String FOOTER =
            "</body>\n" +
            "</html>\n";
    private static String DETAIL =
            "        <div class=\"library\">\n" +
            "        <h1 class=\"title\">%s</h1>\n" +
            "        %s\n" +
            "        %s\n" +
            "        <input type=\"checkbox\"><label></label>\n" +
            "         <div class=\"license\">\n" +
            "         %s\n" +
            "         </div>\n" +
            "        </div>\n";
    private static String NOTICE =
            "        <p class=\"notice\">%s</p>\n";
    private static String URL =
            "        <p><a href=\"%s\">%s</a></p>\n";
    :

    private void step2(Context context, ArrayList<License> arrayLicense, ArrayList<String> arrayString) {
        this.context = context;
        Database database = DatabaseSingleton.getInstance(context);
        licenseDao = database.licenseDao();
        try (ExecutorService executorService = Executors.newSingleThreadExecutor()) {
            executorService.execute(() -> {
                try {
                    for (License license : arrayLicense) {
                        License record = licenseDao.name(license.name);
                        if (record == null) {
                            licenseDao.insert(license);
                        } else {
                            record.version = license.version;
                            licenseDao.update(record);
                        }
                    }
                } catch (Exception e) {
                    Log.d(TAG, Objects.requireNonNull(e.getMessage()));
                }
                String group = null;
                String name = "";
                String copyrightHolder = "";
                String url = "";
                ArrayList<License> arrayList = new ArrayList<>();
                for (String string : arrayString) {
                    if (string.contains("- artifact:")) {
                        String body = string.substring(12);
                        if (group == null) {
                            group = body.substring(0, Math.max(body.indexOf(":"), 0)).trim();
                        } else {
                            arrayList.add(new License(null, group, name, "", copyrightHolder, "", url));
                            group = body.substring(0, Math.max(body.indexOf(":"), 0)).trim();
                            name = "";
                            copyrightHolder = "";
                            url = "";
                        }
                    }
                    if (name.isEmpty() && string.contains("name:")) {
                        name = string.substring(Math.max(string.indexOf("name:") + 6, 0)).trim();
                    }
                    if (copyrightHolder.isEmpty() && string.contains("copyrightHolder:")) {
                        copyrightHolder = string.substring(Math.max(string.indexOf("copyrightHolder:") + 17, 0)).trim();
                    }
                    if (url.isEmpty() && string.contains("url:")) {
                        url = string.substring(Math.max(string.indexOf("url:") + 5, 0)).trim();
                    }
                }
                if (!name.isEmpty()) {
                    arrayList.add(new License(null, group, name, "", copyrightHolder, "", url));
                }
                try {
                    for (License license1 : arrayList) {
                        License record = licenseDao.name(license1.name);
                        if (record != null) {
                            record.group = license1.group;
                            record.copyright = license1.copyright;
                            record.url = license1.url;
                            licenseDao.update(record);
                        }
                    }
                } catch (Exception e) {
                    Log.d(TAG, Objects.requireNonNull(e.getMessage()));
                }
                List<License> licenses = licenseDao.select();
                // YAML出力
                initializeFile(YAML);
                for (License license : licenses) {
                    String string = String.format("- artifact: %s:%s:+\n", license.group, license.name) +
                            String.format("  name: %s\n", license.name) +
                            String.format("  copyrightHolder: %s\n", license.copyright.isEmpty()? "#COPYRIGHT_HOLDER#" : license.copyright) +
                            String.format("  url: %s\n", license.url.isEmpty() ? "#URL#" : license.url);
                    writeFile(string, true);
                }
                // HTML出力
                initializeFile(HTML);
                if (!licenses.isEmpty()) {
                    // HEADER
                    writeFile(HEADER, true);
                    ArrayList<String> arrayDelete = new ArrayList<>();
                    // BODY
                    for (License license : licenses) {
                        if (!license.name.isEmpty()) {
                            StringBuilder body = new StringBuilder();
                            String notice = "";
                            String href = "";
                            if (setFile(license.license)) {
                                ArrayList<String> arrayHtml = readFileString();
                                for (String html : arrayHtml) {
                                    if (arrayHtml.size() == 1 && html.toLowerCase().indexOf("http") == 0) {
                                        body.append(String.format(URL, html, html));
                                        break;
                                    } else {
                                        body.append(String.format("%s\n", html));
                                    }
                                }
                                if (!arrayDelete.contains(license.license)) {
                                    arrayDelete.add(license.license);
                                }
                            }
                            if (!license.copyright.isEmpty() && !license.copyright.equals("#COPYRIGHT_HOLDER#")) {
                                notice = String.format(NOTICE, license.copyright);
                            }
                            if (!license.url.isEmpty() && !license.url.equals("#URL#")) {
                                href = String.format(URL, license.url, license.url);
                            }
                            writeFile(String.format(DETAIL, license.name, notice, href, body), true);
                        }
                    }
                    // FOOTER
                    writeFile(FOOTER, true);
                    // HTML削除
                    for (String html : arrayDelete) {
                        initializeFile(html);
                    }
                }
            });
        }
    }

    public boolean setFile(String fileName) {
        file = new File(context.getFilesDir(), fileName);
        return file.exists();
    }

    public boolean initializeFile(String fileName) {
        file = new File(context.getFilesDir(), fileName);
        return (!file.exists() || file.delete());
    }

    public ArrayList<String> readFileString(File file) {
        ArrayList<String> stringList = new ArrayList<>();
        try (FileInputStream fileInputStream = new FileInputStream(file);
             InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8);
             BufferedReader reader= new BufferedReader(inputStreamReader) ) {
            String lineBuffer;
            while( (lineBuffer = reader.readLine()) != null ) {
                stringList.add(lineBuffer);
            }
        } catch (Exception e) {
            Log.d(TAG, Objects.requireNonNull(e.getMessage()));
        }
        return stringList;
    }

    public void writeFile(String data, boolean mode) {
        try (FileOutputStream fileOutputStream = new FileOutputStream(file, mode);
            OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8);
            BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter)) {
            bufferedWriter.write(data);
            bufferedWriter.flush();
        } catch (Exception e) {
            Log.d(TAG, Objects.requireNonNull(e.getMessage()));
        }
    }

生成したYAMLとHTMLはassetsフォルダにコピーします。
HTMLの著作権者、リンク先を変更する場合、コピー先のYAMLを修正、ライセンス本文のファイル出力を再実行します。

実行方法

ライセンス本文のファイル出力の実行はアプリのMainActivity等の任意の箇所で実行します。

// ライセンス本文のファイル出力
step1(getApplicationContext());

OSSライセンスの表示

生成したHTMLの表示はWebViewを使用します。

fragment_html.xml

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:ignore="VisualLintBounds">
    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp"
        app:cardCornerRadius="8dp"
        app:cardElevation="0dp">
        <WebView
            android:id="@+id/viewer"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/transparent"/>
    </androidx.cardview.widget.CardView>
</FrameLayout>

CardViewを使用して、WebViewを角丸で表示します。

HtmlFragment

public class HtmlFragment extends Fragment {
    public HtmlFragment() {
        // Required empty public constructor
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        :  
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_html, container, false);
        WebView webView = view.findViewById(R.id.viewer);
        WebSettings webSettings = webView.getSettings();
        webSettings.setJavaScriptEnabled(false);
        webSettings.setSupportMultipleWindows(false);
        webView.loadUrl("file:///android_asset/licenses.html");
        return view;
    }
}

loadUrlでassetsフォルダのHTMLを読み込みます。

今回は、ここまでです。

誤字脱字、意味不明でわかりづらい、
もっと詳しく知りたいなどのご意見は、
このページの最後にある
コメントか、
こちらから、お願いいたします♪

ポチッとして頂けると、
次のコンテンツを作成する励みになります♪

ブログランキング・にほんブログ村へ

これからAndroidのアプリ開発やJavaでの開発を始めたい方へ

アプリケーション開発経験がない方や、アプリケーション開発経験がある方でも、JavaやC#などのオブジェクト指向言語が初めての方は、Androidのアプリ開発ができるようになるには、かなりの時間がかかります。
オンラインスクールでの習得を、強くおススメします。

未経験者からプログラマーを目指すのに最適です。まずは無料カウンセリングから♪

ゲーム系に強いスクール、UnityやUnrealEngineを習得するのに最適です。まずは無料オンライン相談から♪

参考になったら、💛をポッチとしてね♪

スポンサーリンク
シェアする
msakiをフォローする
スポンサーリンク

コメント欄

タイトルとURLをコピーしました