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