summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJaekyun Seok <jaekyun@google.com>2017-04-18 15:22:01 +0900
committerJaekyun Seok <jaekyun@google.com>2017-04-29 09:47:24 +0900
commit74812873f41adbd34ab3ff4fbd7dcbb8f6fde233 (patch)
treeb117d2c6a203fd4be6c8ca2e1e808456eed3f18c
parent10158feecf828cd0218208d9fc351da969ad52f8 (diff)
downloadpackages_apps_Settings-74812873f41adbd34ab3ff4fbd7dcbb8f6fde233.tar.gz
packages_apps_Settings-74812873f41adbd34ab3ff4fbd7dcbb8f6fde233.tar.bz2
packages_apps_Settings-74812873f41adbd34ab3ff4fbd7dcbb8f6fde233.zip
Generate license html file from xml files of partitions
Treble-ization requires each partner to store their license information into their own partition because each partition can be updated individually. So each partition will have its own NOTICE.xml.gz, and Settings should be able to generate license html from xml files of partitions. Test: building succeeded and tested on sailfish. make ROBOTEST_FILTER=LicenseHtmlGeneratorFromXmlTest RunSettingsRoboTests make ROBOTEST_FILTER=LicenseHtmlLoaderTest RunSettingsRoboTests make ROBOTEST_FILTER=SettingsLicenseActivityTest RunSettingsRoboTests Bug: 37099941 Change-Id: If797759d300ee20dd43ad8efd7d17b4f7e0c4537
-rw-r--r--src/com/android/settings/LicenseHtmlGeneratorFromXml.java292
-rw-r--r--src/com/android/settings/LicenseHtmlLoader.java110
-rw-r--r--src/com/android/settings/SettingsLicenseActivity.java78
-rw-r--r--tests/robotests/src/com/android/settings/LicenseHtmlGeneratorFromXmlTest.java123
-rw-r--r--tests/robotests/src/com/android/settings/LicenseHtmlLoaderTest.java109
-rw-r--r--tests/robotests/src/com/android/settings/SettingsLicenseActivityTest.java116
6 files changed, 824 insertions, 4 deletions
diff --git a/src/com/android/settings/LicenseHtmlGeneratorFromXml.java b/src/com/android/settings/LicenseHtmlGeneratorFromXml.java
new file mode 100644
index 0000000000..7025c5adf3
--- /dev/null
+++ b/src/com/android/settings/LicenseHtmlGeneratorFromXml.java
@@ -0,0 +1,292 @@
+/*
+ * 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.settings;
+
+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);
+ }
+}
diff --git a/src/com/android/settings/LicenseHtmlLoader.java b/src/com/android/settings/LicenseHtmlLoader.java
new file mode 100644
index 0000000000..9717926901
--- /dev/null
+++ b/src/com/android/settings/LicenseHtmlLoader.java
@@ -0,0 +1,110 @@
+/*
+ * 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.settings;
+
+import android.content.Context;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+
+import com.android.settings.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.
+ */
+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);
+ }
+}
diff --git a/src/com/android/settings/SettingsLicenseActivity.java b/src/com/android/settings/SettingsLicenseActivity.java
index e0b7efe5c6..5b23a68990 100644
--- a/src/com/android/settings/SettingsLicenseActivity.java
+++ b/src/com/android/settings/SettingsLicenseActivity.java
@@ -17,32 +17,87 @@
package com.android.settings;
import android.app.Activity;
+import android.app.LoaderManager;
import android.content.ActivityNotFoundException;
+import android.content.ContentResolver;
+import android.content.Context;
import android.content.Intent;
+import android.content.Loader;
import android.net.Uri;
import android.os.Bundle;
import android.os.StrictMode;
import android.os.SystemProperties;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.content.FileProvider;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
+import com.android.settings.users.RestrictedProfileSettings;
+
import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
/**
* The "dialog" that shows from "License" in the Settings app.
*/
-public class SettingsLicenseActivity extends Activity {
+public class SettingsLicenseActivity extends Activity implements
+ LoaderManager.LoaderCallbacks<File> {
private static final String TAG = "SettingsLicenseActivity";
private static final String DEFAULT_LICENSE_PATH = "/system/etc/NOTICE.html.gz";
private static final String PROPERTY_LICENSE_PATH = "ro.config.license_path";
+ private static final int LOADER_ID_LICENSE_HTML_LOADER = 0;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- final String path = SystemProperties.get(PROPERTY_LICENSE_PATH, DEFAULT_LICENSE_PATH);
+ final String licenseHtmlPath =
+ SystemProperties.get(PROPERTY_LICENSE_PATH, DEFAULT_LICENSE_PATH);
+ if (isFilePathValid(licenseHtmlPath)) {
+ showSelectedFile(licenseHtmlPath);
+ } else {
+ showHtmlFromDefaultXmlFiles();
+ }
+ }
+
+ @Override
+ public Loader<File> onCreateLoader(int id, Bundle args) {
+ return new LicenseHtmlLoader(this);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<File> loader, File generatedHtmlFile) {
+ showGeneratedHtmlFile(generatedHtmlFile);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<File> loader) {
+ }
+
+ private void showHtmlFromDefaultXmlFiles() {
+ getLoaderManager().initLoader(LOADER_ID_LICENSE_HTML_LOADER, Bundle.EMPTY, this);
+ }
+
+ @VisibleForTesting
+ Uri getUriFromGeneratedHtmlFile(File generatedHtmlFile) {
+ return FileProvider.getUriForFile(this, RestrictedProfileSettings.FILE_PROVIDER_AUTHORITY,
+ generatedHtmlFile);
+ }
+
+ private void showGeneratedHtmlFile(File generatedHtmlFile) {
+ if (generatedHtmlFile != null) {
+ showHtmlFromUri(getUriFromGeneratedHtmlFile(generatedHtmlFile));
+ } else {
+ Log.e(TAG, "Failed to generate.");
+ showErrorAndFinish();
+ }
+ }
+
+ private void showSelectedFile(final String path) {
if (TextUtils.isEmpty(path)) {
Log.e(TAG, "The system property for the license file is empty");
showErrorAndFinish();
@@ -50,18 +105,24 @@ public class SettingsLicenseActivity extends Activity {
}
final File file = new File(path);
- if (!file.exists() || file.length() == 0) {
+ if (!isFileValid(file)) {
Log.e(TAG, "License file " + path + " does not exist");
showErrorAndFinish();
return;
}
+ showHtmlFromUri(Uri.fromFile(file));
+ }
+ private void showHtmlFromUri(Uri uri) {
// Kick off external viewer due to WebView security restrictions; we
// carefully point it at HTMLViewer, since it offers to decompress
// before viewing.
final Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setDataAndType(Uri.fromFile(file), "text/html");
+ intent.setDataAndType(uri, "text/html");
intent.putExtra(Intent.EXTRA_TITLE, getString(R.string.settings_license_activity_title));
+ if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setPackage("com.android.htmlviewer");
@@ -79,4 +140,13 @@ public class SettingsLicenseActivity extends Activity {
.show();
finish();
}
+
+ private boolean isFilePathValid(final String path) {
+ return !TextUtils.isEmpty(path) && isFileValid(new File(path));
+ }
+
+ @VisibleForTesting
+ boolean isFileValid(final File file) {
+ return file.exists() && file.length() != 0;
+ }
}
diff --git a/tests/robotests/src/com/android/settings/LicenseHtmlGeneratorFromXmlTest.java b/tests/robotests/src/com/android/settings/LicenseHtmlGeneratorFromXmlTest.java
new file mode 100644
index 0000000000..ef36e5f5f0
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/LicenseHtmlGeneratorFromXmlTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.settings;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+import org.xmlpull.v1.XmlPullParserException;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class LicenseHtmlGeneratorFromXmlTest {
+ private static final String VAILD_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 INVAILD_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(VAILD_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(INVAILD_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);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/LicenseHtmlLoaderTest.java b/tests/robotests/src/com/android/settings/LicenseHtmlLoaderTest.java
new file mode 100644
index 0000000000..96e88c2339
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/LicenseHtmlLoaderTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.settings;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.annotation.Config;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class LicenseHtmlLoaderTest {
+ @Mock
+ private Context mContext;
+
+ LicenseHtmlLoader newLicenseHtmlLoader(ArrayList<File> xmlFiles,
+ File cachedHtmlFile, boolean isCachedHtmlFileOutdated,
+ boolean generateHtmlFileSucceeded) {
+ LicenseHtmlLoader loader = spy(new LicenseHtmlLoader(mContext));
+ doReturn(xmlFiles).when(loader).getVaildXmlFiles();
+ doReturn(cachedHtmlFile).when(loader).getCachedHtmlFile();
+ doReturn(isCachedHtmlFileOutdated).when(loader).isCachedHtmlFileOutdated(any(), any());
+ doReturn(generateHtmlFileSucceeded).when(loader).generateHtmlFile(any(), any());
+ return loader;
+ }
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void testLoadInBackground() {
+ ArrayList<File> xmlFiles = new ArrayList();
+ xmlFiles.add(new File("test.xml"));
+ File cachedHtmlFile = new File("test.html");
+
+ LicenseHtmlLoader loader = newLicenseHtmlLoader(xmlFiles, cachedHtmlFile, true, true);
+
+ assertThat(loader.loadInBackground()).isEqualTo(cachedHtmlFile);
+ verify(loader).generateHtmlFile(any(), any());
+ }
+
+ @Test
+ public void testLoadInBackgroundWithNoVaildXmlFiles() {
+ ArrayList<File> xmlFiles = new ArrayList();
+ File cachedHtmlFile = new File("test.html");
+
+ LicenseHtmlLoader loader = newLicenseHtmlLoader(xmlFiles, cachedHtmlFile, true, true);
+
+ assertThat(loader.loadInBackground()).isNull();
+ verify(loader, never()).generateHtmlFile(any(), any());
+ }
+
+ @Test
+ public void testLoadInBackgroundWithNonOutdatedCachedHtmlFile() {
+ ArrayList<File> xmlFiles = new ArrayList();
+ xmlFiles.add(new File("test.xml"));
+ File cachedHtmlFile = new File("test.html");
+
+ LicenseHtmlLoader loader = newLicenseHtmlLoader(xmlFiles, cachedHtmlFile, false, true);
+
+ assertThat(loader.loadInBackground()).isEqualTo(cachedHtmlFile);
+ verify(loader, never()).generateHtmlFile(any(), any());
+ }
+
+ @Test
+ public void testLoadInBackgroundWithGenerateHtmlFileFailed() {
+ ArrayList<File> xmlFiles = new ArrayList();
+ xmlFiles.add(new File("test.xml"));
+ File cachedHtmlFile = new File("test.html");
+
+ LicenseHtmlLoader loader = newLicenseHtmlLoader(xmlFiles, cachedHtmlFile, true, false);
+
+ assertThat(loader.loadInBackground()).isNull();
+ verify(loader).generateHtmlFile(any(), any());
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/SettingsLicenseActivityTest.java b/tests/robotests/src/com/android/settings/SettingsLicenseActivityTest.java
new file mode 100644
index 0000000000..3e28a2a9d5
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/SettingsLicenseActivityTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.settings;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.app.Application;
+import android.os.Bundle;
+import android.os.SystemProperties;
+import android.content.Context;
+import android.content.Intent;
+import android.content.Loader;
+import android.net.Uri;
+import android.os.Bundle;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.res.builder.RobolectricPackageManager;
+import org.robolectric.util.ActivityController;
+import org.robolectric.shadows.ShadowActivity;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class SettingsLicenseActivityTest {
+ private ActivityController<SettingsLicenseActivity> mActivityController;
+ private SettingsLicenseActivity mActivity;
+ private Application mApplication;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mApplication = RuntimeEnvironment.application;
+ mActivityController = Robolectric.buildActivity(SettingsLicenseActivity.class);
+ mActivity = spy(mActivityController.get());
+ }
+
+ void assertEqualIntents(Intent actual, Intent expected) {
+ assertThat(actual.getAction()).isEqualTo(expected.getAction());
+ assertThat(actual.getDataString()).isEqualTo(expected.getDataString());
+ assertThat(actual.getType()).isEqualTo(expected.getType());
+ assertThat(actual.getCategories()).isEqualTo(expected.getCategories());
+ assertThat(actual.getPackage()).isEqualTo(expected.getPackage());
+ assertThat(actual.getFlags()).isEqualTo(expected.getFlags());
+ }
+
+ @Test
+ public void testOnCreateWithValidHtmlFile() {
+ SystemProperties.set("ro.config.license_path", "/system/etc/NOTICE.html.gz");
+
+ doReturn(true).when(mActivity).isFileValid(any());
+ mActivity.onCreate(null);
+
+ final Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(Uri.parse("file:///system/etc/NOTICE.html.gz"), "text/html");
+ intent.putExtra(Intent.EXTRA_TITLE, mActivity.getString(
+ R.string.settings_license_activity_title));
+ intent.addCategory(Intent.CATEGORY_DEFAULT);
+ intent.setPackage("com.android.htmlviewer");
+
+ assertEqualIntents(shadowOf(mApplication).getNextStartedActivity(), intent);
+ }
+
+ @Test
+ public void testOnCreateWithGeneratedHtmlFile() {
+ doReturn(null).when(mActivity).onCreateLoader(anyInt(), any());
+ doReturn(Uri.parse("content://com.android.settings.files/my_cache/generated_test.html"))
+ .when(mActivity).getUriFromGeneratedHtmlFile(any());
+
+ mActivity.onCreate(null);
+ mActivity.onLoadFinished(null, new File("/generated_test.html"));
+
+ final Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(
+ Uri.parse("content://com.android.settings.files/my_cache/generated_test.html"),
+ "text/html");
+ intent.putExtra(Intent.EXTRA_TITLE, mActivity.getString(
+ R.string.settings_license_activity_title));
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.addCategory(Intent.CATEGORY_DEFAULT);
+ intent.setPackage("com.android.htmlviewer");
+
+ assertEqualIntents(shadowOf(mApplication).getNextStartedActivity(), intent);
+ }
+}