1 /* 2 * Copyright (C) 2019 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.deviceinfo.legal; 18 19 import android.content.ContentProvider; 20 import android.content.ContentResolver; 21 import android.content.ContentValues; 22 import android.content.Context; 23 import android.content.SharedPreferences; 24 import android.content.pm.PackageInfo; 25 import android.content.pm.PackageManager; 26 import android.content.res.AssetManager; 27 import android.database.Cursor; 28 import android.net.Uri; 29 import android.os.ParcelFileDescriptor; 30 import android.util.Log; 31 32 import androidx.annotation.VisibleForTesting; 33 import androidx.core.util.Preconditions; 34 35 import java.io.File; 36 import java.io.IOException; 37 import java.io.InputStream; 38 import java.nio.file.Files; 39 import java.nio.file.StandardCopyOption; 40 import java.util.List; 41 import java.util.zip.GZIPInputStream; 42 43 public class ModuleLicenseProvider extends ContentProvider { 44 private static final String TAG = "ModuleLicenseProvider"; 45 46 public static final String AUTHORITY = "com.android.settings.module_licenses"; 47 static final String GZIPPED_LICENSE_FILE_NAME = "NOTICE.html.gz"; 48 static final String LICENSE_FILE_NAME = "NOTICE.html"; 49 static final String LICENSE_FILE_MIME_TYPE = "text/html"; 50 static final String PREFS_NAME = "ModuleLicenseProvider"; 51 52 @Override onCreate()53 public boolean onCreate() { 54 return true; 55 } 56 57 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)58 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 59 String sortOrder) { 60 throw new UnsupportedOperationException(); 61 } 62 63 @Override getType(Uri uri)64 public String getType(Uri uri) { 65 checkUri(getContext(), uri); 66 return LICENSE_FILE_MIME_TYPE; 67 } 68 69 @Override insert(Uri uri, ContentValues values)70 public Uri insert(Uri uri, ContentValues values) { 71 throw new UnsupportedOperationException(); 72 } 73 74 @Override delete(Uri uri, String selection, String[] selectionArgs)75 public int delete(Uri uri, String selection, String[] selectionArgs) { 76 throw new UnsupportedOperationException(); 77 } 78 79 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)80 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 81 throw new UnsupportedOperationException(); 82 } 83 84 @Override openFile(Uri uri, String mode)85 public ParcelFileDescriptor openFile(Uri uri, String mode) { 86 final Context context = getContext(); 87 checkUri(context, uri); 88 Preconditions.checkArgument("r".equals(mode), "Read is the only supported mode"); 89 90 try { 91 String packageName = uri.getPathSegments().get(0); 92 File cachedFile = getCachedHtmlFile(context, packageName); 93 if (isCachedHtmlFileOutdated(context, packageName)) { 94 try (InputStream in = new GZIPInputStream( 95 getPackageAssetManager(context.getPackageManager(), packageName) 96 .open(GZIPPED_LICENSE_FILE_NAME))) { 97 File directory = getCachedFileDirectory(context, packageName); 98 if (!directory.exists()) { 99 directory.mkdir(); 100 } 101 Files.copy(in, cachedFile.toPath(), StandardCopyOption.REPLACE_EXISTING); 102 } 103 // Now that the file is saved, write the package's version code to shared prefs 104 SharedPreferences.Editor editor = getPrefs(context).edit(); 105 editor.putLong( 106 packageName, 107 getPackageInfo(context, packageName).getLongVersionCode()) 108 .commit(); 109 } 110 return ParcelFileDescriptor.open(cachedFile, ParcelFileDescriptor.MODE_READ_ONLY); 111 } catch (PackageManager.NameNotFoundException e) { 112 Log.wtf(TAG, "checkUri should have already caught this error", e); 113 } catch (IOException e) { 114 Log.e(TAG, "Could not open file descriptor", e); 115 } 116 return null; 117 } 118 119 /** 120 * Returns true if the cached file for the given package is outdated. A cached file is 121 * outdated if one of the following are true: 122 * 1. the shared prefs does not contain a version code for this package 123 * 2. The version code does not match the package's version code 124 * 3. There is no file or the file is empty. 125 */ 126 @VisibleForTesting isCachedHtmlFileOutdated(Context context, String packageName)127 static boolean isCachedHtmlFileOutdated(Context context, String packageName) 128 throws PackageManager.NameNotFoundException { 129 SharedPreferences prefs = getPrefs(context); 130 File file = getCachedHtmlFile(context, packageName); 131 return !prefs.contains(packageName) 132 || prefs.getLong(packageName, 0L) 133 != getPackageInfo(context, packageName).getLongVersionCode() 134 || !file.exists() || file.length() == 0; 135 } 136 getPackageAssetManager(PackageManager packageManager, String packageName)137 static AssetManager getPackageAssetManager(PackageManager packageManager, String packageName) 138 throws PackageManager.NameNotFoundException { 139 return packageManager.getResourcesForApplication( 140 packageManager.getPackageInfo(packageName, PackageManager.MATCH_APEX) 141 .applicationInfo) 142 .getAssets(); 143 } 144 getUriForPackage(String packageName)145 static Uri getUriForPackage(String packageName) { 146 return new Uri.Builder() 147 .scheme(ContentResolver.SCHEME_CONTENT) 148 .authority(AUTHORITY) 149 .appendPath(packageName) 150 .appendPath(LICENSE_FILE_NAME) 151 .build(); 152 } 153 checkUri(Context context, Uri uri)154 private static void checkUri(Context context, Uri uri) { 155 List<String> pathSegments = uri.getPathSegments(); 156 // A URI is valid iff it: 157 // 1. is a content URI 158 // 2. uses the correct authority 159 // 3. has exactly 2 segments and the last one is NOTICE.html 160 // 4. (checked below) first path segment is the package name of a module 161 if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()) 162 || !AUTHORITY.equals(uri.getAuthority()) 163 || pathSegments == null 164 || pathSegments.size() != 2 165 || !LICENSE_FILE_NAME.equals(pathSegments.get(1))) { 166 throw new IllegalArgumentException(uri + "is not a valid URI"); 167 } 168 // Grab the first path segment, which is the package name of the module and make sure that 169 // there's actually a module for that package. getModuleInfo will throw if it does not 170 // exist. 171 try { 172 context.getPackageManager().getModuleInfo(pathSegments.get(0), 0 /* flags */); 173 } catch (PackageManager.NameNotFoundException e) { 174 throw new IllegalArgumentException(uri + "is not a valid URI", e); 175 } 176 } 177 getCachedFileDirectory(Context context, String packageName)178 private static File getCachedFileDirectory(Context context, String packageName) { 179 return new File(context.getCacheDir(), packageName); 180 } 181 getCachedHtmlFile(Context context, String packageName)182 private static File getCachedHtmlFile(Context context, String packageName) { 183 return new File(context.getCacheDir() + "/" + packageName, LICENSE_FILE_NAME); 184 } 185 getPackageInfo(Context context, String packageName)186 private static PackageInfo getPackageInfo(Context context, String packageName) 187 throws PackageManager.NameNotFoundException { 188 return context.getPackageManager().getPackageInfo(packageName, PackageManager.MATCH_APEX); 189 } 190 getPrefs(Context context)191 private static SharedPreferences getPrefs(Context context) { 192 return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); 193 } 194 } 195