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