Neil Fuller | f68ad8c | 2017-06-05 13:54:44 +0100 | [diff] [blame] | 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 | |
Neil Fuller | f68ad8c | 2017-06-05 13:54:44 +0100 | [diff] [blame] | 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; |
Neil Fuller | dabb14f | 2017-09-22 17:01:10 +0100 | [diff] [blame] | 34 | import android.os.UserHandle; |
Neil Fuller | f68ad8c | 2017-06-05 13:54:44 +0100 | [diff] [blame] | 35 | import android.provider.TimeZoneRulesDataContract; |
Neil Fuller | 9f7d290 | 2017-07-10 19:21:31 +0100 | [diff] [blame] | 36 | import android.provider.TimeZoneRulesDataContract.Operation; |
Filip Pavlis | 1bc4d5c | 2018-03-29 01:45:09 +0100 | [diff] [blame^] | 37 | import androidx.annotation.NonNull; |
| 38 | import androidx.annotation.Nullable; |
Neil Fuller | f68ad8c | 2017-06-05 13:54:44 +0100 | [diff] [blame] | 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; |
Neil Fuller | f68ad8c | 2017-06-05 13:54:44 +0100 | [diff] [blame] | 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<>(); |
Neil Fuller | 0005999 | 2017-07-11 15:27:35 +0100 | [diff] [blame] | 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); |
Neil Fuller | f68ad8c | 2017-06-05 13:54:44 +0100 | [diff] [blame] | 76 | KNOWN_COLUMN_NAMES = Collections.unmodifiableSet(columnNames); |
| 77 | |
| 78 | Map<String, Class<?>> columnTypes = new HashMap<>(); |
Neil Fuller | 0005999 | 2017-07-11 15:27:35 +0100 | [diff] [blame] | 79 | 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 Fuller | f68ad8c | 2017-06-05 13:54:44 +0100 | [diff] [blame] | 84 | 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 Fuller | dabb14f | 2017-09-22 17:01:10 +0100 | [diff] [blame] | 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 | |
Neil Fuller | f68ad8c | 2017-06-05 13:54:44 +0100 | [diff] [blame] | 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 | } |
Neil Fuller | 3cdcb52 | 2017-08-11 14:19:32 +0100 | [diff] [blame] | 123 | if (!android.Manifest.permission.UPDATE_TIME_ZONE_RULES.equals(info.readPermission)) { |
Neil Fuller | f68ad8c | 2017-06-05 13:54:44 +0100 | [diff] [blame] | 124 | // Writing is not supported. |
| 125 | throw new SecurityException("android:readPermission must be set to \"" |
Neil Fuller | 3cdcb52 | 2017-08-11 14:19:32 +0100 | [diff] [blame] | 126 | + android.Manifest.permission.UPDATE_TIME_ZONE_RULES |
Neil Fuller | f68ad8c | 2017-06-05 13:54:44 +0100 | [diff] [blame] | 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 | |
Neil Fuller | 9f7d290 | 2017-07-10 19:21:31 +0100 | [diff] [blame] | 138 | // Work out what the operation type is. |
| 139 | String type; |
Neil Fuller | f68ad8c | 2017-06-05 13:54:44 +0100 | [diff] [blame] | 140 | try { |
Neil Fuller | 9f7d290 | 2017-07-10 19:21:31 +0100 | [diff] [blame] | 141 | type = getMandatoryMetaDataString(metaData, METADATA_KEY_OPERATION); |
Neil Fuller | 0005999 | 2017-07-11 15:27:35 +0100 | [diff] [blame] | 142 | mColumnData.put(Operation.COLUMN_TYPE, type); |
Neil Fuller | f68ad8c | 2017-06-05 13:54:44 +0100 | [diff] [blame] | 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. |
Neil Fuller | 9f7d290 | 2017-07-10 19:21:31 +0100 | [diff] [blame] | 148 | if (Operation.TYPE_INSTALL.equals(type)) { |
Neil Fuller | f68ad8c | 2017-06-05 13:54:44 +0100 | [diff] [blame] | 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(); |
Neil Fuller | 0005999 | 2017-07-11 15:27:35 +0100 | [diff] [blame] | 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); |
Neil Fuller | f68ad8c | 2017-06-05 13:54:44 +0100 | [diff] [blame] | 166 | } 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 Fuller | 9f7d290 | 2017-07-10 19:21:31 +0100 | [diff] [blame] | 176 | if (!Operation.CONTENT_URI.equals(uri)) { |
Neil Fuller | f68ad8c | 2017-06-05 13:54:44 +0100 | [diff] [blame] | 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 { |
Neil Fuller | 0005999 | 2017-07-11 15:27:35 +0100 | [diff] [blame] | 263 | if (!Operation.CONTENT_URI.equals(uri)) { |
Neil Fuller | f68ad8c | 2017-06-05 13:54:44 +0100 | [diff] [blame] | 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 | } |