Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d69dfff0 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Move codes generating html file from xml files to SettingsLib (2/2)"

parents ef4f9d9d 8d923f0b
Loading
Loading
Loading
Loading
+292 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settingslib.license;

import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
import android.util.Xml;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPInputStream;

/**
 * The utility class that generate a license html file from xml files.
 * All the HTML snippets and logic are copied from build/make/tools/generate-notice-files.py.
 *
 * TODO: Remove duplicate codes once backward support ends.
 */
class LicenseHtmlGeneratorFromXml {
    private static final String TAG = "LicenseHtmlGeneratorFromXml";

    private static final String TAG_ROOT = "licenses";
    private static final String TAG_FILE_NAME = "file-name";
    private static final String TAG_FILE_CONTENT = "file-content";
    private static final String ATTR_CONTENT_ID = "contentId";

    private static final String HTML_HEAD_STRING =
            "<html><head>\n"
            + "<style type=\"text/css\">\n"
            + "body { padding: 0; font-family: sans-serif; }\n"
            + ".same-license { background-color: #eeeeee;\n"
            + "                border-top: 20px solid white;\n"
            + "                padding: 10px; }\n"
            + ".label { font-weight: bold; }\n"
            + ".file-list { margin-left: 1em; color: blue; }\n"
            + "</style>\n"
            + "</head>"
            + "<body topmargin=\"0\" leftmargin=\"0\" rightmargin=\"0\" bottommargin=\"0\">\n"
            + "<div class=\"toc\">\n"
            + "<ul>";

    private static final String HTML_MIDDLE_STRING =
            "</ul>\n"
            + "</div><!-- table of contents -->\n"
            + "<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\">";

    private static final String HTML_REAR_STRING =
            "</table></body></html>";

    private final List<File> mXmlFiles;

    /*
     * A map from a file name to a content id (MD5 sum of file content) for its license.
     * For example, "/system/priv-app/TeleService/TeleService.apk" maps to
     * "9645f39e9db895a4aa6e02cb57294595". Here "9645f39e9db895a4aa6e02cb57294595" is a MD5 sum
     * of the content of packages/services/Telephony/MODULE_LICENSE_APACHE2.
     */
    private final Map<String, String> mFileNameToContentIdMap = new HashMap();

    /*
     * A map from a content id (MD5 sum of file content) to a license file content.
     * For example, "9645f39e9db895a4aa6e02cb57294595" maps to the content string of
     * packages/services/Telephony/MODULE_LICENSE_APACHE2. Here "9645f39e9db895a4aa6e02cb57294595"
     * is a MD5 sum of the file content.
     */
    private final Map<String, String> mContentIdToFileContentMap = new HashMap();

    static class ContentIdAndFileNames {
        final String mContentId;
        final List<String> mFileNameList = new ArrayList();

        ContentIdAndFileNames(String contentId) {
            mContentId = contentId;
        }
    }

    private LicenseHtmlGeneratorFromXml(List<File> xmlFiles) {
        mXmlFiles = xmlFiles;
    }

    public static boolean generateHtml(List<File> xmlFiles, File outputFile) {
        LicenseHtmlGeneratorFromXml genertor = new LicenseHtmlGeneratorFromXml(xmlFiles);
        return genertor.generateHtml(outputFile);
    }

    private boolean generateHtml(File outputFile) {
        for (File xmlFile : mXmlFiles) {
            parse(xmlFile);
        }

        if (mFileNameToContentIdMap.isEmpty() || mContentIdToFileContentMap.isEmpty()) {
            return false;
        }

        PrintWriter writer = null;
        try {
            writer = new PrintWriter(outputFile);

            generateHtml(mFileNameToContentIdMap, mContentIdToFileContentMap, writer);

            writer.flush();
            writer.close();
            return true;
        } catch (FileNotFoundException | SecurityException e) {
            Log.e(TAG, "Failed to generate " + outputFile, e);

            if (writer != null) {
                writer.close();
            }
            return false;
        }
    }

    private void parse(File xmlFile) {
        if (xmlFile == null || !xmlFile.exists() || xmlFile.length() == 0) {
            return;
        }

        InputStreamReader in = null;
        try {
            if (xmlFile.getName().endsWith(".gz")) {
                in = new InputStreamReader(new GZIPInputStream(new FileInputStream(xmlFile)));
            } else {
                in = new FileReader(xmlFile);
            }

            parse(in, mFileNameToContentIdMap, mContentIdToFileContentMap);

            in.close();
        } catch (XmlPullParserException | IOException e) {
            Log.e(TAG, "Failed to parse " + xmlFile, e);
            if (in != null) {
                try {
                    in.close();
                } catch (IOException ie) {
                    Log.w(TAG, "Failed to close " + xmlFile);
                }
            }
        }
    }

    /*
     * Parses an input stream and fills a map from a file name to a content id for its license
     * and a map from a content id to a license file content.
     *
     * Following xml format is expected from the input stream.
     *
     *     <licenses>
     *     <file-name contentId="content_id_of_license1">file1</file-name>
     *     <file-name contentId="content_id_of_license2">file2</file-name>
     *     ...
     *     <file-content contentId="content_id_of_license1">license1 file contents</file-content>
     *     <file-content contentId="content_id_of_license2">license2 file contents</file-content>
     *     ...
     *     </licenses>
     */
    @VisibleForTesting
    static void parse(InputStreamReader in, Map<String, String> outFileNameToContentIdMap,
            Map<String, String> outContentIdToFileContentMap)
                    throws XmlPullParserException, IOException {
        Map<String, String> fileNameToContentIdMap = new HashMap<String, String>();
        Map<String, String> contentIdToFileContentMap = new HashMap<String, String>();

        XmlPullParser parser = Xml.newPullParser();
        parser.setInput(in);
        parser.nextTag();

        parser.require(XmlPullParser.START_TAG, "", TAG_ROOT);

        int state = parser.getEventType();
        while (state != XmlPullParser.END_DOCUMENT) {
            if (state == XmlPullParser.START_TAG) {
                if (TAG_FILE_NAME.equals(parser.getName())) {
                    String contentId = parser.getAttributeValue("", ATTR_CONTENT_ID);
                    if (!TextUtils.isEmpty(contentId)) {
                        String fileName = readText(parser).trim();
                        if (!TextUtils.isEmpty(fileName)) {
                            fileNameToContentIdMap.put(fileName, contentId);
                        }
                    }
                } else if (TAG_FILE_CONTENT.equals(parser.getName())) {
                    String contentId = parser.getAttributeValue("", ATTR_CONTENT_ID);
                    if (!TextUtils.isEmpty(contentId)
                            && !outContentIdToFileContentMap.containsKey(contentId)
                            && !contentIdToFileContentMap.containsKey(contentId)) {
                        String fileContent = readText(parser);
                        if (!TextUtils.isEmpty(fileContent)) {
                            contentIdToFileContentMap.put(contentId, fileContent);
                        }
                    }
                }
            }

            state = parser.next();
        }
        outFileNameToContentIdMap.putAll(fileNameToContentIdMap);
        outContentIdToFileContentMap.putAll(contentIdToFileContentMap);
    }

    private static String readText(XmlPullParser parser)
            throws IOException, XmlPullParserException {
        StringBuffer result = new StringBuffer();
        int state = parser.next();
        while (state == XmlPullParser.TEXT) {
            result.append(parser.getText());
            state = parser.next();
        }
        return result.toString();
    }

    @VisibleForTesting
    static void generateHtml(Map<String, String> fileNameToContentIdMap,
            Map<String, String> contentIdToFileContentMap, PrintWriter writer) {
        List<String> fileNameList = new ArrayList();
        fileNameList.addAll(fileNameToContentIdMap.keySet());
        Collections.sort(fileNameList);

        writer.println(HTML_HEAD_STRING);

        int count = 0;
        Map<String, Integer> contentIdToOrderMap = new HashMap();
        List<ContentIdAndFileNames> contentIdAndFileNamesList = new ArrayList();

        // Prints all the file list with a link to its license file content.
        for (String fileName : fileNameList) {
            String contentId = fileNameToContentIdMap.get(fileName);
            // Assigns an id to a newly referred license file content.
            if (!contentIdToOrderMap.containsKey(contentId)) {
                contentIdToOrderMap.put(contentId, count);

                // An index in contentIdAndFileNamesList is the order of each element.
                contentIdAndFileNamesList.add(new ContentIdAndFileNames(contentId));
                count++;
            }

            int id = contentIdToOrderMap.get(contentId);
            contentIdAndFileNamesList.get(id).mFileNameList.add(fileName);
            writer.format("<li><a href=\"#id%d\">%s</a></li>\n", id, fileName);
        }

        writer.println(HTML_MIDDLE_STRING);

        count = 0;
        // Prints all contents of the license files in order of id.
        for (ContentIdAndFileNames contentIdAndFileNames : contentIdAndFileNamesList) {
            writer.format("<tr id=\"id%d\"><td class=\"same-license\">\n", count);
            writer.println("<div class=\"label\">Notices for file(s):</div>");
            writer.println("<div class=\"file-list\">");
            for (String fileName : contentIdAndFileNames.mFileNameList) {
                writer.format("%s <br/>\n", fileName);
            }
            writer.println("</div><!-- file-list -->");
            writer.println("<pre class=\"license-text\">");
            writer.println(contentIdToFileContentMap.get(
                    contentIdAndFileNames.mContentId));
            writer.println("</pre><!-- license-text -->");
            writer.println("</td></tr><!-- same-license -->");

            count++;
        }

        writer.println(HTML_REAR_STRING);
    }
}
+110 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settingslib.license;

import android.content.Context;
import android.support.annotation.VisibleForTesting;
import android.util.Log;

import com.android.settingslib.utils.AsyncLoader;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

/**
 * LicenseHtmlLoader is a loader which loads a license html file from default license xml files.
 */
public class LicenseHtmlLoader extends AsyncLoader<File> {
    private static final String TAG = "LicenseHtmlLoader";

    private static final String[] DEFAULT_LICENSE_XML_PATHS = {
            "/system/etc/NOTICE.xml.gz",
            "/vendor/etc/NOTICE.xml.gz",
            "/odm/etc/NOTICE.xml.gz",
            "/oem/etc/NOTICE.xml.gz"};
    private static final String NOTICE_HTML_FILE_NAME = "NOTICE.html";

    private Context mContext;

    public LicenseHtmlLoader(Context context) {
        super(context);
        mContext = context;
    }

    @Override
    public File loadInBackground() {
        return generateHtmlFromDefaultXmlFiles();
    }

    @Override
    protected void onDiscardResult(File f) {
    }

    private File generateHtmlFromDefaultXmlFiles() {
        final List<File> xmlFiles = getVaildXmlFiles();
        if (xmlFiles.isEmpty()) {
            Log.e(TAG, "No notice file exists.");
            return null;
        }

        File cachedHtmlFile = getCachedHtmlFile();
        if (!isCachedHtmlFileOutdated(xmlFiles, cachedHtmlFile)
                || generateHtmlFile(xmlFiles, cachedHtmlFile)) {
            return cachedHtmlFile;
        }

        return null;
    }

    @VisibleForTesting
    List<File> getVaildXmlFiles() {
        final List<File> xmlFiles = new ArrayList();
        for (final String xmlPath : DEFAULT_LICENSE_XML_PATHS) {
            File file = new File(xmlPath);
            if (file.exists() && file.length() != 0) {
                xmlFiles.add(file);
            }
        }
        return xmlFiles;
    }

    @VisibleForTesting
    File getCachedHtmlFile() {
        return new File(mContext.getCacheDir(), NOTICE_HTML_FILE_NAME);
    }

    @VisibleForTesting
    boolean isCachedHtmlFileOutdated(List<File> xmlFiles, File cachedHtmlFile) {
        boolean outdated = true;
        if (cachedHtmlFile.exists() && cachedHtmlFile.length() != 0) {
            outdated = false;
            for (File file : xmlFiles) {
                if (cachedHtmlFile.lastModified() < file.lastModified()) {
                    outdated = true;
                    break;
                }
            }
        }
        return outdated;
    }

    @VisibleForTesting
    boolean generateHtmlFile(List<File> xmlFiles, File htmlFile) {
        return LicenseHtmlGeneratorFromXml.generateHtml(xmlFiles, htmlFile);
    }
}
+110 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settingslib.utils;

import android.content.AsyncTaskLoader;
import android.content.Context;

/**
 * This class fills in some boilerplate for AsyncTaskLoader to actually load things.
 *
 * Subclasses need to implement {@link AsyncLoader#loadInBackground()} to perform the actual
 * background task, and {@link AsyncLoader#onDiscardResult(T)} to clean up previously loaded
 * results.
 *
 * This loader is based on the MailAsyncTaskLoader from the AOSP EmailUnified repo.
 *
 * @param <T> the data type to be loaded.
 */
public abstract class AsyncLoader<T> extends AsyncTaskLoader<T> {
    private T mResult;

    public AsyncLoader(final Context context) {
        super(context);
    }

    @Override
    protected void onStartLoading() {
        if (mResult != null) {
            deliverResult(mResult);
        }

        if (takeContentChanged() || mResult == null) {
            forceLoad();
        }
    }

    @Override
    protected void onStopLoading() {
        cancelLoad();
    }

    @Override
    public void deliverResult(final T data) {
        if (isReset()) {
            if (data != null) {
                onDiscardResult(data);
            }
            return;
        }

        final T oldResult = mResult;
        mResult = data;

        if (isStarted()) {
            super.deliverResult(data);
        }

        if (oldResult != null && oldResult != mResult) {
            onDiscardResult(oldResult);
        }
    }

    @Override
    protected void onReset() {
        super.onReset();

        onStopLoading();

        if (mResult != null) {
            onDiscardResult(mResult);
        }
        mResult = null;
    }

    @Override
    public void onCanceled(final T data) {
        super.onCanceled(data);

        if (data != null) {
            onDiscardResult(data);
        }
    }

    /**
     * Called when discarding the load results so subclasses can take care of clean-up or
     * recycling tasks. This is not called if the same result (by way of pointer equality) is
     * returned again by a subsequent call to loadInBackground, or if result is null.
     *
     * Note that this may be called concurrently with loadInBackground(), and in some circumstances
     * may be called more than once for a given object.
     *
     * @param result The value returned from {@link AsyncLoader#loadInBackground()} which
     *               is to be discarded.
     */
    protected abstract void onDiscardResult(T result);
}
+126 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settingslib.license;

import static com.google.common.truth.Truth.assertThat;

import com.android.settingslib.TestConfig;
import com.android.settingslib.SettingsLibRobolectricTestRunner;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import org.xmlpull.v1.XmlPullParserException;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;

@RunWith(SettingsLibRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
public class LicenseHtmlGeneratorFromXmlTest {
    private static final String VALILD_XML_STRING =
            "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
            + "<licenses>\n"
            + "<file-name contentId=\"0\">/file0</file-name>\n"
            + "<file-name contentId=\"0\">/file1</file-name>\n"
            + "<file-content contentId=\"0\"><![CDATA[license content #0]]></file-content>\n"
            + "</licenses>";

    private static final String INVALILD_XML_STRING =
            "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
            + "<licenses2>\n"
            + "<file-name contentId=\"0\">/file0</file-name>\n"
            + "<file-name contentId=\"0\">/file1</file-name>\n"
            + "<file-content contentId=\"0\"><![CDATA[license content #0]]></file-content>\n"
            + "</licenses2>";

    private static final String EXPECTED_HTML_STRING =
            "<html><head>\n"
            + "<style type=\"text/css\">\n"
            + "body { padding: 0; font-family: sans-serif; }\n"
            + ".same-license { background-color: #eeeeee;\n"
            + "                border-top: 20px solid white;\n"
            + "                padding: 10px; }\n"
            + ".label { font-weight: bold; }\n"
            + ".file-list { margin-left: 1em; color: blue; }\n"
            + "</style>\n"
            + "</head>"
            + "<body topmargin=\"0\" leftmargin=\"0\" rightmargin=\"0\" bottommargin=\"0\">\n"
            + "<div class=\"toc\">\n"
            + "<ul>\n"
            + "<li><a href=\"#id0\">/file0</a></li>\n"
            + "<li><a href=\"#id0\">/file1</a></li>\n"
            + "</ul>\n"
            + "</div><!-- table of contents -->\n"
            + "<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n"
            + "<tr id=\"id0\"><td class=\"same-license\">\n"
            + "<div class=\"label\">Notices for file(s):</div>\n"
            + "<div class=\"file-list\">\n"
            + "/file0 <br/>\n"
            + "/file1 <br/>\n"
            + "</div><!-- file-list -->\n"
            + "<pre class=\"license-text\">\n"
            + "license content #0\n"
            + "</pre><!-- license-text -->\n"
            + "</td></tr><!-- same-license -->\n"
            + "</table></body></html>\n";

    @Test
    public void testParseValidXmlStream() throws XmlPullParserException, IOException {
        Map<String, String> fileNameToContentIdMap = new HashMap<String, String>();
        Map<String, String> contentIdToFileContentMap = new HashMap<String, String>();

        LicenseHtmlGeneratorFromXml.parse(
                new InputStreamReader(new ByteArrayInputStream(VALILD_XML_STRING.getBytes())),
                fileNameToContentIdMap, contentIdToFileContentMap);
        assertThat(fileNameToContentIdMap.size()).isEqualTo(2);
        assertThat(fileNameToContentIdMap.get("/file0")).isEqualTo("0");
        assertThat(fileNameToContentIdMap.get("/file1")).isEqualTo("0");
        assertThat(contentIdToFileContentMap.size()).isEqualTo(1);
        assertThat(contentIdToFileContentMap.get("0")).isEqualTo("license content #0");
    }

    @Test(expected = XmlPullParserException.class)
    public void testParseInvalidXmlStream() throws XmlPullParserException, IOException {
        Map<String, String> fileNameToContentIdMap = new HashMap<String, String>();
        Map<String, String> contentIdToFileContentMap = new HashMap<String, String>();

        LicenseHtmlGeneratorFromXml.parse(
                new InputStreamReader(new ByteArrayInputStream(INVALILD_XML_STRING.getBytes())),
                fileNameToContentIdMap, contentIdToFileContentMap);
    }

    @Test
    public void testGenerateHtml() {
        Map<String, String> fileNameToContentIdMap = new HashMap<String, String>();
        Map<String, String> contentIdToFileContentMap = new HashMap<String, String>();

        fileNameToContentIdMap.put("/file0", "0");
        fileNameToContentIdMap.put("/file1", "0");
        contentIdToFileContentMap.put("0", "license content #0");

        StringWriter output = new StringWriter();
        LicenseHtmlGeneratorFromXml.generateHtml(
                fileNameToContentIdMap, contentIdToFileContentMap, new PrintWriter(output));
        assertThat(output.toString()).isEqualTo(EXPECTED_HTML_STRING);
    }
}
+110 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading