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<>(); 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); 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 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 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