• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.timezone.data;
18 
19 import com.android.timezone.distro.DistroException;
20 import com.android.timezone.distro.DistroVersion;
21 import com.android.timezone.distro.TimeZoneDistro;
22 
23 import android.content.ContentProvider;
24 import android.content.ContentValues;
25 import android.content.Context;
26 import android.content.pm.PackageManager;
27 import android.content.pm.ProviderInfo;
28 import android.content.res.AssetManager;
29 import android.database.AbstractCursor;
30 import android.database.Cursor;
31 import android.net.Uri;
32 import android.os.Bundle;
33 import android.os.ParcelFileDescriptor;
34 import android.os.UserHandle;
35 import android.provider.TimeZoneRulesDataContract;
36 import android.provider.TimeZoneRulesDataContract.Operation;
37 import androidx.annotation.NonNull;
38 import androidx.annotation.Nullable;
39 
40 import java.io.File;
41 import java.io.FileNotFoundException;
42 import java.io.FileOutputStream;
43 import java.io.IOException;
44 import java.io.InputStream;
45 import java.io.OutputStream;
46 import java.util.Arrays;
47 import java.util.Collections;
48 import java.util.HashMap;
49 import java.util.HashSet;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.Set;
53 
54 import static android.content.res.AssetManager.ACCESS_STREAMING;
55 
56 /**
57  * A basic implementation of a time zone data provider that can be used by OEMs to implement
58  * an APK asset-based solution for time zone updates.
59  */
60 public final class TimeZoneRulesDataProvider extends ContentProvider {
61 
62     static final String TAG = "TimeZoneRulesDataProvider";
63 
64     private static final String METADATA_KEY_OPERATION = "android.timezoneprovider.OPERATION";
65 
66     private static final Set<String> KNOWN_COLUMN_NAMES;
67     private static final Map<String, Class<?>> KNOWN_COLUMN_TYPES;
68 
69     static {
70         Set<String> columnNames = new HashSet<>();
71         columnNames.add(Operation.COLUMN_TYPE);
72         columnNames.add(Operation.COLUMN_DISTRO_MAJOR_VERSION);
73         columnNames.add(Operation.COLUMN_DISTRO_MINOR_VERSION);
74         columnNames.add(Operation.COLUMN_RULES_VERSION);
75         columnNames.add(Operation.COLUMN_REVISION);
76         KNOWN_COLUMN_NAMES = Collections.unmodifiableSet(columnNames);
77 
78         Map<String, Class<?>> columnTypes = new HashMap<>();
columnTypes.put(Operation.COLUMN_TYPE, String.class)79         columnTypes.put(Operation.COLUMN_TYPE, String.class);
columnTypes.put(Operation.COLUMN_DISTRO_MAJOR_VERSION, Integer.class)80         columnTypes.put(Operation.COLUMN_DISTRO_MAJOR_VERSION, Integer.class);
columnTypes.put(Operation.COLUMN_DISTRO_MINOR_VERSION, Integer.class)81         columnTypes.put(Operation.COLUMN_DISTRO_MINOR_VERSION, Integer.class);
columnTypes.put(Operation.COLUMN_RULES_VERSION, String.class)82         columnTypes.put(Operation.COLUMN_RULES_VERSION, String.class);
columnTypes.put(Operation.COLUMN_REVISION, Integer.class)83         columnTypes.put(Operation.COLUMN_REVISION, Integer.class);
84         KNOWN_COLUMN_TYPES = Collections.unmodifiableMap(columnTypes);
85     }
86 
87     private final Map<String, Object> mColumnData = new HashMap<>();
88 
89     @Override
onCreate()90     public boolean onCreate() {
91         return true;
92     }
93 
94     @Override
attachInfo(Context context, ProviderInfo info)95     public void attachInfo(Context context, ProviderInfo info) {
96         super.attachInfo(context, info);
97 
98         // The time zone update process should run as the system user exclusively as it's a
99         // system feature, not user dependent.
100         UserHandle currentUserHandle = android.os.Process.myUserHandle();
101         if (!currentUserHandle.isSystem()) {
102             throw new SecurityException("ContentProvider is supposed to run as the system user,"
103                     + " instead user=" + currentUserHandle);
104         }
105 
106         // Sanity check our security
107         if (!TimeZoneRulesDataContract.AUTHORITY.equals(info.authority)) {
108             // The authority looked for by the time zone updater is fixed.
109             throw new SecurityException(
110                     "android:authorities must be \"" + TimeZoneRulesDataContract.AUTHORITY + "\"");
111         }
112         if (!info.grantUriPermissions) {
113             throw new SecurityException("Provider must grant uri permissions");
114         }
115         if (!info.exported) {
116             // The content provider is accessed directly so must be exported.
117             throw new SecurityException("android:exported must be \"true\"");
118         }
119         if (info.pathPermissions != null || info.writePermission != null) {
120             // Use readPermission only to implement permissions.
121             throw new SecurityException("Use android:readPermission only");
122         }
123         if (!android.Manifest.permission.UPDATE_TIME_ZONE_RULES.equals(info.readPermission)) {
124             // Writing is not supported.
125             throw new SecurityException("android:readPermission must be set to \""
126                     + android.Manifest.permission.UPDATE_TIME_ZONE_RULES
127                     + "\" is: " + info.readPermission);
128         }
129 
130         // info.metadata is not filled in by default. Must ask for it again.
131         final ProviderInfo infoWithMetadata = context.getPackageManager()
132                 .resolveContentProvider(info.authority, PackageManager.GET_META_DATA);
133         Bundle metaData = infoWithMetadata.metaData;
134         if (metaData == null) {
135             throw new SecurityException("meta-data must be set");
136         }
137 
138         // Work out what the operation type is.
139         String type;
140         try {
141             type = getMandatoryMetaDataString(metaData, METADATA_KEY_OPERATION);
142             mColumnData.put(Operation.COLUMN_TYPE, type);
143         } catch (IllegalArgumentException e) {
144             throw new SecurityException(METADATA_KEY_OPERATION + " meta-data not set.");
145         }
146 
147         // Fill in version information if this is an install operation.
148         if (Operation.TYPE_INSTALL.equals(type)) {
149             // Extract the version information from the distro.
150             InputStream distroBytesInputStream;
151             try {
152                 distroBytesInputStream = context.getAssets().open(TimeZoneDistro.FILE_NAME);
153             } catch (IOException e) {
154                 throw new SecurityException(
155                         "Unable to open asset: " + TimeZoneDistro.FILE_NAME, e);
156             }
157             TimeZoneDistro distro = new TimeZoneDistro(distroBytesInputStream);
158             try {
159                 DistroVersion distroVersion = distro.getDistroVersion();
160                 mColumnData.put(Operation.COLUMN_DISTRO_MAJOR_VERSION,
161                         distroVersion.formatMajorVersion);
162                 mColumnData.put(Operation.COLUMN_DISTRO_MINOR_VERSION,
163                         distroVersion.formatMinorVersion);
164                 mColumnData.put(Operation.COLUMN_RULES_VERSION, distroVersion.rulesVersion);
165                 mColumnData.put(Operation.COLUMN_REVISION, distroVersion.revision);
166             } catch (IOException | DistroException e) {
167                 throw new SecurityException("Invalid asset: " + TimeZoneDistro.FILE_NAME, e);
168             }
169 
170         }
171     }
172 
173     @Override
query(@onNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder)174     public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
175             @Nullable String[] selectionArgs, @Nullable String sortOrder) {
176         if (!Operation.CONTENT_URI.equals(uri)) {
177             return null;
178         }
179         final List<String> projectionList = Arrays.asList(projection);
180         if (projection != null && !KNOWN_COLUMN_NAMES.containsAll(projectionList)) {
181             throw new UnsupportedOperationException(
182                     "Only " + KNOWN_COLUMN_NAMES + " columns supported.");
183         }
184 
185         return new AbstractCursor() {
186             @Override
187             public int getCount() {
188                 return 1;
189             }
190 
191             @Override
192             public String[] getColumnNames() {
193                 return projectionList.toArray(new String[0]);
194             }
195 
196             @Override
197             public int getType(int column) {
198                 String columnName = projectionList.get(column);
199                 Class<?> columnJavaType = KNOWN_COLUMN_TYPES.get(columnName);
200                 if (columnJavaType == String.class) {
201                     return Cursor.FIELD_TYPE_STRING;
202                 } else if (columnJavaType == Integer.class) {
203                     return Cursor.FIELD_TYPE_INTEGER;
204                 } else {
205                     throw new UnsupportedOperationException(
206                             "Unsupported type: " + columnJavaType + " for " + columnName);
207                 }
208             }
209 
210             @Override
211             public String getString(int column) {
212                 checkPosition();
213                 String columnName = projectionList.get(column);
214                 if (KNOWN_COLUMN_TYPES.get(columnName) != String.class) {
215                     throw new UnsupportedOperationException();
216                 }
217                 return (String) mColumnData.get(columnName);
218             }
219 
220             @Override
221             public short getShort(int column) {
222                 checkPosition();
223                 throw new UnsupportedOperationException();
224             }
225 
226             @Override
227             public int getInt(int column) {
228                 checkPosition();
229                 String columnName = projectionList.get(column);
230                 if (KNOWN_COLUMN_TYPES.get(columnName) != Integer.class) {
231                     throw new UnsupportedOperationException();
232                 }
233                 return (Integer) mColumnData.get(columnName);
234             }
235 
236             @Override
237             public long getLong(int column) {
238                 return getInt(column);
239             }
240 
241             @Override
242             public float getFloat(int column) {
243                 throw new UnsupportedOperationException();
244             }
245 
246             @Override
247             public double getDouble(int column) {
248                 checkPosition();
249                 throw new UnsupportedOperationException();
250             }
251 
252             @Override
253             public boolean isNull(int column) {
254                 checkPosition();
255                 return column != 0;
256             }
257         };
258     }
259 
260     @Override
261     public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
262             throws FileNotFoundException {
263         if (!Operation.CONTENT_URI.equals(uri)) {
264             throw new FileNotFoundException("Unknown URI: " + uri);
265         }
266         if (!"r".equals(mode)) {
267             throw new FileNotFoundException("Only read-only access supported.");
268         }
269 
270         // We cannot return the asset ParcelFileDescriptor from
271         // assets.openFd(name).getParcelFileDescriptor() here as the receiver in the reading
272         // process gets a ParcelFileDescriptor pointing at the whole .apk. Instead, we extract
273         // the asset file we want to storage then wrap that in a ParcelFileDescriptor.
274         File distroFile = null;
275         try {
276             distroFile = File.createTempFile("distro", null, getContext().getFilesDir());
277 
278             AssetManager assets = getContext().getAssets();
279             try (InputStream is = assets.open(TimeZoneDistro.FILE_NAME, ACCESS_STREAMING);
280                  FileOutputStream fos = new FileOutputStream(distroFile, false /* append */)) {
281                 copy(is, fos);
282             }
283 
284             return ParcelFileDescriptor.open(distroFile, ParcelFileDescriptor.MODE_READ_ONLY);
285         } catch (IOException e) {
286             throw new RuntimeException("Unable to copy distro asset file", e);
287         } finally {
288             if (distroFile != null) {
289                 // Even if we have an open file descriptor pointing at the file it should be safe to
290                 // delete because of normal Unix file behavior. Deleting here avoids leaking any
291                 // storage.
292                 distroFile.delete();
293             }
294         }
295     }
296 
297     @Override
298     public String getType(@NonNull Uri uri) {
299         return null;
300     }
301 
302     @Override
303     public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
304         throw new UnsupportedOperationException();
305     }
306 
307     @Override
308     public int delete(@NonNull Uri uri, @Nullable String selection,
309             @Nullable String[] selectionArgs) {
310         throw new UnsupportedOperationException();
311     }
312 
313     @Override
314     public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection,
315             @Nullable String[] selectionArgs) {
316         throw new UnsupportedOperationException();
317     }
318 
319     private static String getMandatoryMetaDataString(Bundle metaData, String key) {
320         if (!metaData.containsKey(key)) {
321             throw new SecurityException("No metadata with key " + key + " found.");
322         }
323         return metaData.getString(key);
324     }
325 
326     /**
327      * Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed.
328      */
329     private static void copy(InputStream in, OutputStream out) throws IOException {
330         byte[] buffer = new byte[8192];
331         int c;
332         while ((c = in.read(buffer)) != -1) {
333             out.write(buffer, 0, c);
334         }
335     }
336 }
337