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