blob: 0c1b39c8d0ed86fa87418811a1249c9424dc2611 [file] [log] [blame]
Neil Fullerf68ad8c2017-06-05 13:54:44 +01001/*
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
17package com.android.timezone.data;
18
19import com.android.timezone.distro.DistroException;
20import com.android.timezone.distro.DistroVersion;
21import com.android.timezone.distro.TimeZoneDistro;
22
Neil Fullerf68ad8c2017-06-05 13:54:44 +010023import android.content.ContentProvider;
24import android.content.ContentValues;
25import android.content.Context;
26import android.content.pm.PackageManager;
27import android.content.pm.ProviderInfo;
28import android.content.res.AssetManager;
29import android.database.AbstractCursor;
30import android.database.Cursor;
31import android.net.Uri;
32import android.os.Bundle;
33import android.os.ParcelFileDescriptor;
Neil Fullerdabb14f2017-09-22 17:01:10 +010034import android.os.UserHandle;
Neil Fullerf68ad8c2017-06-05 13:54:44 +010035import android.provider.TimeZoneRulesDataContract;
Neil Fuller9f7d2902017-07-10 19:21:31 +010036import android.provider.TimeZoneRulesDataContract.Operation;
Filip Pavlis1bc4d5c2018-03-29 01:45:09 +010037import androidx.annotation.NonNull;
38import androidx.annotation.Nullable;
Neil Fullerf68ad8c2017-06-05 13:54:44 +010039
40import java.io.File;
41import java.io.FileNotFoundException;
42import java.io.FileOutputStream;
43import java.io.IOException;
44import java.io.InputStream;
45import java.io.OutputStream;
46import java.util.Arrays;
47import java.util.Collections;
48import java.util.HashMap;
49import java.util.HashSet;
50import java.util.List;
51import java.util.Map;
52import java.util.Set;
53
54import static android.content.res.AssetManager.ACCESS_STREAMING;
Neil Fullerf68ad8c2017-06-05 13:54:44 +010055
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 */
60public 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<>();
Neil Fuller00059992017-07-11 15:27:35 +010071 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);
Neil Fullerf68ad8c2017-06-05 13:54:44 +010076 KNOWN_COLUMN_NAMES = Collections.unmodifiableSet(columnNames);
77
78 Map<String, Class<?>> columnTypes = new HashMap<>();
Neil Fuller00059992017-07-11 15:27:35 +010079 columnTypes.put(Operation.COLUMN_TYPE, String.class);
80 columnTypes.put(Operation.COLUMN_DISTRO_MAJOR_VERSION, Integer.class);
81 columnTypes.put(Operation.COLUMN_DISTRO_MINOR_VERSION, Integer.class);
82 columnTypes.put(Operation.COLUMN_RULES_VERSION, String.class);
83 columnTypes.put(Operation.COLUMN_REVISION, Integer.class);
Neil Fullerf68ad8c2017-06-05 13:54:44 +010084 KNOWN_COLUMN_TYPES = Collections.unmodifiableMap(columnTypes);
85 }
86
87 private final Map<String, Object> mColumnData = new HashMap<>();
88
89 @Override
90 public boolean onCreate() {
91 return true;
92 }
93
94 @Override
95 public void attachInfo(Context context, ProviderInfo info) {
96 super.attachInfo(context, info);
97
Neil Fullerdabb14f2017-09-22 17:01:10 +010098 // 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
Neil Fullerf68ad8c2017-06-05 13:54:44 +0100106 // 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 }
Neil Fuller3cdcb522017-08-11 14:19:32 +0100123 if (!android.Manifest.permission.UPDATE_TIME_ZONE_RULES.equals(info.readPermission)) {
Neil Fullerf68ad8c2017-06-05 13:54:44 +0100124 // Writing is not supported.
125 throw new SecurityException("android:readPermission must be set to \""
Neil Fuller3cdcb522017-08-11 14:19:32 +0100126 + android.Manifest.permission.UPDATE_TIME_ZONE_RULES
Neil Fullerf68ad8c2017-06-05 13:54:44 +0100127 + "\" 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
Neil Fuller9f7d2902017-07-10 19:21:31 +0100138 // Work out what the operation type is.
139 String type;
Neil Fullerf68ad8c2017-06-05 13:54:44 +0100140 try {
Neil Fuller9f7d2902017-07-10 19:21:31 +0100141 type = getMandatoryMetaDataString(metaData, METADATA_KEY_OPERATION);
Neil Fuller00059992017-07-11 15:27:35 +0100142 mColumnData.put(Operation.COLUMN_TYPE, type);
Neil Fullerf68ad8c2017-06-05 13:54:44 +0100143 } 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.
Neil Fuller9f7d2902017-07-10 19:21:31 +0100148 if (Operation.TYPE_INSTALL.equals(type)) {
Neil Fullerf68ad8c2017-06-05 13:54:44 +0100149 // 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();
Neil Fuller00059992017-07-11 15:27:35 +0100160 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);
Neil Fullerf68ad8c2017-06-05 13:54:44 +0100166 } catch (IOException | DistroException e) {
167 throw new SecurityException("Invalid asset: " + TimeZoneDistro.FILE_NAME, e);
168 }
169
170 }
171 }
172
173 @Override
174 public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
175 @Nullable String[] selectionArgs, @Nullable String sortOrder) {
Neil Fuller9f7d2902017-07-10 19:21:31 +0100176 if (!Operation.CONTENT_URI.equals(uri)) {
Neil Fullerf68ad8c2017-06-05 13:54:44 +0100177 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 {
Neil Fuller00059992017-07-11 15:27:35 +0100263 if (!Operation.CONTENT_URI.equals(uri)) {
Neil Fullerf68ad8c2017-06-05 13:54:44 +0100264 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}