1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings; 18 19 import android.support.annotation.VisibleForTesting; 20 import android.text.TextUtils; 21 import android.util.Log; 22 import android.util.Xml; 23 24 import org.xmlpull.v1.XmlPullParser; 25 import org.xmlpull.v1.XmlPullParserException; 26 27 import java.io.File; 28 import java.io.FileInputStream; 29 import java.io.FileNotFoundException; 30 import java.io.FileReader; 31 import java.io.IOException; 32 import java.io.InputStreamReader; 33 import java.io.PrintWriter; 34 import java.util.ArrayList; 35 import java.util.Collections; 36 import java.util.HashMap; 37 import java.util.List; 38 import java.util.Map; 39 import java.util.zip.GZIPInputStream; 40 41 /** 42 * The utility class that generate a license html file from xml files. 43 * All the HTML snippets and logic are copied from build/make/tools/generate-notice-files.py. 44 * 45 * TODO: Remove duplicate codes once backward support ends. 46 */ 47 class LicenseHtmlGeneratorFromXml { 48 private static final String TAG = "LicenseHtmlGeneratorFromXml"; 49 50 private static final String TAG_ROOT = "licenses"; 51 private static final String TAG_FILE_NAME = "file-name"; 52 private static final String TAG_FILE_CONTENT = "file-content"; 53 private static final String ATTR_CONTENT_ID = "contentId"; 54 55 private static final String HTML_HEAD_STRING = 56 "<html><head>\n" + 57 "<style type=\"text/css\">\n" + 58 "body { padding: 0; font-family: sans-serif; }\n" + 59 ".same-license { background-color: #eeeeee;\n" + 60 " border-top: 20px solid white;\n" + 61 " padding: 10px; }\n" + 62 ".label { font-weight: bold; }\n" + 63 ".file-list { margin-left: 1em; color: blue; }\n" + 64 "</style>\n" + 65 "</head>" + 66 "<body topmargin=\"0\" leftmargin=\"0\" rightmargin=\"0\" bottommargin=\"0\">\n" + 67 "<div class=\"toc\">\n" + 68 "<ul>"; 69 70 private static final String HTML_MIDDLE_STRING = 71 "</ul>\n" + 72 "</div><!-- table of contents -->\n" + 73 "<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\">"; 74 75 private static final String HTML_REAR_STRING = 76 "</table></body></html>"; 77 78 private final List<File> mXmlFiles; 79 80 /* 81 * A map from a file name to a content id (MD5 sum of file content) for its license. 82 * For example, "/system/priv-app/TeleService/TeleService.apk" maps to 83 * "9645f39e9db895a4aa6e02cb57294595". Here "9645f39e9db895a4aa6e02cb57294595" is a MD5 sum 84 * of the content of packages/services/Telephony/MODULE_LICENSE_APACHE2. 85 */ 86 private final Map<String, String> mFileNameToContentIdMap = new HashMap(); 87 88 /* 89 * A map from a content id (MD5 sum of file content) to a license file content. 90 * For example, "9645f39e9db895a4aa6e02cb57294595" maps to the content string of 91 * packages/services/Telephony/MODULE_LICENSE_APACHE2. Here "9645f39e9db895a4aa6e02cb57294595" 92 * is a MD5 sum of the file content. 93 */ 94 private final Map<String, String> mContentIdToFileContentMap = new HashMap(); 95 96 static class ContentIdAndFileNames { 97 final String mContentId; 98 final List<String> mFileNameList = new ArrayList(); 99 ContentIdAndFileNames(String contentId)100 ContentIdAndFileNames(String contentId) { 101 mContentId = contentId; 102 } 103 } 104 LicenseHtmlGeneratorFromXml(List<File> xmlFiles)105 private LicenseHtmlGeneratorFromXml(List<File> xmlFiles) { 106 mXmlFiles = xmlFiles; 107 } 108 generateHtml(List<File> xmlFiles, File outputFile)109 public static boolean generateHtml(List<File> xmlFiles, File outputFile) { 110 LicenseHtmlGeneratorFromXml genertor = new LicenseHtmlGeneratorFromXml(xmlFiles); 111 return genertor.generateHtml(outputFile); 112 } 113 generateHtml(File outputFile)114 private boolean generateHtml(File outputFile) { 115 for (File xmlFile : mXmlFiles) { 116 parse(xmlFile); 117 } 118 119 if (mFileNameToContentIdMap.isEmpty() || mContentIdToFileContentMap.isEmpty()) { 120 return false; 121 } 122 123 PrintWriter writer = null; 124 try { 125 writer = new PrintWriter(outputFile); 126 127 generateHtml(mFileNameToContentIdMap, mContentIdToFileContentMap, writer); 128 129 writer.flush(); 130 writer.close(); 131 return true; 132 } catch (FileNotFoundException | SecurityException e) { 133 Log.e(TAG, "Failed to generate " + outputFile, e); 134 135 if (writer != null) { 136 writer.close(); 137 } 138 return false; 139 } 140 } 141 parse(File xmlFile)142 private void parse(File xmlFile) { 143 if (xmlFile == null || !xmlFile.exists() || xmlFile.length() == 0) { 144 return; 145 } 146 147 InputStreamReader in = null; 148 try { 149 if (xmlFile.getName().endsWith(".gz")) { 150 in = new InputStreamReader(new GZIPInputStream(new FileInputStream(xmlFile))); 151 } else { 152 in = new FileReader(xmlFile); 153 } 154 155 parse(in, mFileNameToContentIdMap, mContentIdToFileContentMap); 156 157 in.close(); 158 } catch (XmlPullParserException | IOException e) { 159 Log.e(TAG, "Failed to parse " + xmlFile, e); 160 if (in != null) { 161 try { 162 in.close(); 163 } catch (IOException ie) { 164 Log.w(TAG, "Failed to close " + xmlFile); 165 } 166 } 167 } 168 } 169 170 /* 171 * Parses an input stream and fills a map from a file name to a content id for its license 172 * and a map from a content id to a license file content. 173 * 174 * Following xml format is expected from the input stream. 175 * 176 * <licenses> 177 * <file-name contentId="content_id_of_license1">file1</file-name> 178 * <file-name contentId="content_id_of_license2">file2</file-name> 179 * ... 180 * <file-content contentId="content_id_of_license1">license1 file contents</file-content> 181 * <file-content contentId="content_id_of_license2">license2 file contents</file-content> 182 * ... 183 * </licenses> 184 */ 185 @VisibleForTesting parse(InputStreamReader in, Map<String, String> outFileNameToContentIdMap, Map<String, String> outContentIdToFileContentMap)186 static void parse(InputStreamReader in, Map<String, String> outFileNameToContentIdMap, 187 Map<String, String> outContentIdToFileContentMap) 188 throws XmlPullParserException, IOException { 189 Map<String, String> fileNameToContentIdMap = new HashMap<String, String>(); 190 Map<String, String> contentIdToFileContentMap = new HashMap<String, String>(); 191 192 XmlPullParser parser = Xml.newPullParser(); 193 parser.setInput(in); 194 parser.nextTag(); 195 196 parser.require(XmlPullParser.START_TAG, "", TAG_ROOT); 197 198 int state = parser.getEventType(); 199 while (state != XmlPullParser.END_DOCUMENT) { 200 if (state == XmlPullParser.START_TAG) { 201 if (TAG_FILE_NAME.equals(parser.getName())) { 202 String contentId = parser.getAttributeValue("", ATTR_CONTENT_ID); 203 if (!TextUtils.isEmpty(contentId)) { 204 String fileName = readText(parser).trim(); 205 if (!TextUtils.isEmpty(fileName)) { 206 fileNameToContentIdMap.put(fileName, contentId); 207 } 208 } 209 } else if (TAG_FILE_CONTENT.equals(parser.getName())) { 210 String contentId = parser.getAttributeValue("", ATTR_CONTENT_ID); 211 if (!TextUtils.isEmpty(contentId) && 212 !outContentIdToFileContentMap.containsKey(contentId) && 213 !contentIdToFileContentMap.containsKey(contentId)) { 214 String fileContent = readText(parser); 215 if (!TextUtils.isEmpty(fileContent)) { 216 contentIdToFileContentMap.put(contentId, fileContent); 217 } 218 } 219 } 220 } 221 222 state = parser.next(); 223 } 224 outFileNameToContentIdMap.putAll(fileNameToContentIdMap); 225 outContentIdToFileContentMap.putAll(contentIdToFileContentMap); 226 } 227 readText(XmlPullParser parser)228 private static String readText(XmlPullParser parser) 229 throws IOException, XmlPullParserException { 230 StringBuffer result = new StringBuffer(); 231 int state = parser.next(); 232 while (state == XmlPullParser.TEXT) { 233 result.append(parser.getText()); 234 state = parser.next(); 235 } 236 return result.toString(); 237 } 238 239 @VisibleForTesting generateHtml(Map<String, String> fileNameToContentIdMap, Map<String, String> contentIdToFileContentMap, PrintWriter writer)240 static void generateHtml(Map<String, String> fileNameToContentIdMap, 241 Map<String, String> contentIdToFileContentMap, PrintWriter writer) { 242 List<String> fileNameList = new ArrayList(); 243 fileNameList.addAll(fileNameToContentIdMap.keySet()); 244 Collections.sort(fileNameList); 245 246 writer.println(HTML_HEAD_STRING); 247 248 int count = 0; 249 Map<String, Integer> contentIdToOrderMap = new HashMap(); 250 List<ContentIdAndFileNames> contentIdAndFileNamesList = new ArrayList(); 251 252 // Prints all the file list with a link to its license file content. 253 for (String fileName : fileNameList) { 254 String contentId = fileNameToContentIdMap.get(fileName); 255 // Assigns an id to a newly referred license file content. 256 if (!contentIdToOrderMap.containsKey(contentId)) { 257 contentIdToOrderMap.put(contentId, count); 258 259 // An index in contentIdAndFileNamesList is the order of each element. 260 contentIdAndFileNamesList.add(new ContentIdAndFileNames(contentId)); 261 count++; 262 } 263 264 int id = contentIdToOrderMap.get(contentId); 265 contentIdAndFileNamesList.get(id).mFileNameList.add(fileName); 266 writer.format("<li><a href=\"#id%d\">%s</a></li>\n", id, fileName); 267 } 268 269 writer.println(HTML_MIDDLE_STRING); 270 271 count = 0; 272 // Prints all contents of the license files in order of id. 273 for (ContentIdAndFileNames contentIdAndFileNames : contentIdAndFileNamesList) { 274 writer.format("<tr id=\"id%d\"><td class=\"same-license\">\n", count); 275 writer.println("<div class=\"label\">Notices for file(s):</div>"); 276 writer.println("<div class=\"file-list\">"); 277 for (String fileName : contentIdAndFileNames.mFileNameList) { 278 writer.format("%s <br/>\n", fileName); 279 } 280 writer.println("</div><!-- file-list -->"); 281 writer.println("<pre class=\"license-text\">"); 282 writer.println(contentIdToFileContentMap.get( 283 contentIdAndFileNames.mContentId)); 284 writer.println("</pre><!-- license-text -->"); 285 writer.println("</td></tr><!-- same-license -->"); 286 287 count++; 288 } 289 290 writer.println(HTML_REAR_STRING); 291 } 292 } 293