/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.timezone.data; import com.android.timezone.distro.DistroException; import com.android.timezone.distro.DistroVersion; import com.android.timezone.distro.TimeZoneDistro; import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.content.res.AssetManager; import android.database.AbstractCursor; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.os.UserHandle; import android.provider.TimeZoneRulesDataContract; import android.provider.TimeZoneRulesDataContract.Operation; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import static android.content.res.AssetManager.ACCESS_STREAMING; /** * A basic implementation of a time zone data provider that can be used by OEMs to implement * an APK asset-based solution for time zone updates. */ public final class TimeZoneRulesDataProvider extends ContentProvider { static final String TAG = "TimeZoneRulesDataProvider"; private static final String METADATA_KEY_OPERATION = "android.timezoneprovider.OPERATION"; private static final Set KNOWN_COLUMN_NAMES; private static final Map> KNOWN_COLUMN_TYPES; static { Set columnNames = new HashSet<>(); columnNames.add(Operation.COLUMN_TYPE); columnNames.add(Operation.COLUMN_DISTRO_MAJOR_VERSION); columnNames.add(Operation.COLUMN_DISTRO_MINOR_VERSION); columnNames.add(Operation.COLUMN_RULES_VERSION); columnNames.add(Operation.COLUMN_REVISION); KNOWN_COLUMN_NAMES = Collections.unmodifiableSet(columnNames); Map> columnTypes = new HashMap<>(); columnTypes.put(Operation.COLUMN_TYPE, String.class); columnTypes.put(Operation.COLUMN_DISTRO_MAJOR_VERSION, Integer.class); columnTypes.put(Operation.COLUMN_DISTRO_MINOR_VERSION, Integer.class); columnTypes.put(Operation.COLUMN_RULES_VERSION, String.class); columnTypes.put(Operation.COLUMN_REVISION, Integer.class); KNOWN_COLUMN_TYPES = Collections.unmodifiableMap(columnTypes); } private final Map mColumnData = new HashMap<>(); @Override public boolean onCreate() { return true; } @Override public void attachInfo(Context context, ProviderInfo info) { super.attachInfo(context, info); // The time zone update process should run as the system user exclusively as it's a // system feature, not user dependent. UserHandle currentUserHandle = android.os.Process.myUserHandle(); if (!currentUserHandle.isSystem()) { throw new SecurityException("ContentProvider is supposed to run as the system user," + " instead user=" + currentUserHandle); } // Sanity check our security if (!TimeZoneRulesDataContract.AUTHORITY.equals(info.authority)) { // The authority looked for by the time zone updater is fixed. throw new SecurityException( "android:authorities must be \"" + TimeZoneRulesDataContract.AUTHORITY + "\""); } if (!info.grantUriPermissions) { throw new SecurityException("Provider must grant uri permissions"); } if (!info.exported) { // The content provider is accessed directly so must be exported. throw new SecurityException("android:exported must be \"true\""); } if (info.pathPermissions != null || info.writePermission != null) { // Use readPermission only to implement permissions. throw new SecurityException("Use android:readPermission only"); } if (!android.Manifest.permission.UPDATE_TIME_ZONE_RULES.equals(info.readPermission)) { // Writing is not supported. throw new SecurityException("android:readPermission must be set to \"" + android.Manifest.permission.UPDATE_TIME_ZONE_RULES + "\" is: " + info.readPermission); } // info.metadata is not filled in by default. Must ask for it again. final ProviderInfo infoWithMetadata = context.getPackageManager() .resolveContentProvider(info.authority, PackageManager.GET_META_DATA); Bundle metaData = infoWithMetadata.metaData; if (metaData == null) { throw new SecurityException("meta-data must be set"); } // Work out what the operation type is. String type; try { type = getMandatoryMetaDataString(metaData, METADATA_KEY_OPERATION); mColumnData.put(Operation.COLUMN_TYPE, type); } catch (IllegalArgumentException e) { throw new SecurityException(METADATA_KEY_OPERATION + " meta-data not set."); } // Fill in version information if this is an install operation. if (Operation.TYPE_INSTALL.equals(type)) { // Extract the version information from the distro. InputStream distroBytesInputStream; try { distroBytesInputStream = context.getAssets().open(TimeZoneDistro.FILE_NAME); } catch (IOException e) { throw new SecurityException( "Unable to open asset: " + TimeZoneDistro.FILE_NAME, e); } TimeZoneDistro distro = new TimeZoneDistro(distroBytesInputStream); try { DistroVersion distroVersion = distro.getDistroVersion(); mColumnData.put(Operation.COLUMN_DISTRO_MAJOR_VERSION, distroVersion.formatMajorVersion); mColumnData.put(Operation.COLUMN_DISTRO_MINOR_VERSION, distroVersion.formatMinorVersion); mColumnData.put(Operation.COLUMN_RULES_VERSION, distroVersion.rulesVersion); mColumnData.put(Operation.COLUMN_REVISION, distroVersion.revision); } catch (IOException | DistroException e) { throw new SecurityException("Invalid asset: " + TimeZoneDistro.FILE_NAME, e); } } } @Override public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { if (!Operation.CONTENT_URI.equals(uri)) { return null; } final List projectionList = Arrays.asList(projection); if (projection != null && !KNOWN_COLUMN_NAMES.containsAll(projectionList)) { throw new UnsupportedOperationException( "Only " + KNOWN_COLUMN_NAMES + " columns supported."); } return new AbstractCursor() { @Override public int getCount() { return 1; } @Override public String[] getColumnNames() { return projectionList.toArray(new String[0]); } @Override public int getType(int column) { String columnName = projectionList.get(column); Class columnJavaType = KNOWN_COLUMN_TYPES.get(columnName); if (columnJavaType == String.class) { return Cursor.FIELD_TYPE_STRING; } else if (columnJavaType == Integer.class) { return Cursor.FIELD_TYPE_INTEGER; } else { throw new UnsupportedOperationException( "Unsupported type: " + columnJavaType + " for " + columnName); } } @Override public String getString(int column) { checkPosition(); String columnName = projectionList.get(column); if (KNOWN_COLUMN_TYPES.get(columnName) != String.class) { throw new UnsupportedOperationException(); } return (String) mColumnData.get(columnName); } @Override public short getShort(int column) { checkPosition(); throw new UnsupportedOperationException(); } @Override public int getInt(int column) { checkPosition(); String columnName = projectionList.get(column); if (KNOWN_COLUMN_TYPES.get(columnName) != Integer.class) { throw new UnsupportedOperationException(); } return (Integer) mColumnData.get(columnName); } @Override public long getLong(int column) { return getInt(column); } @Override public float getFloat(int column) { throw new UnsupportedOperationException(); } @Override public double getDouble(int column) { checkPosition(); throw new UnsupportedOperationException(); } @Override public boolean isNull(int column) { checkPosition(); return column != 0; } }; } @Override public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException { if (!Operation.CONTENT_URI.equals(uri)) { throw new FileNotFoundException("Unknown URI: " + uri); } if (!"r".equals(mode)) { throw new FileNotFoundException("Only read-only access supported."); } // We cannot return the asset ParcelFileDescriptor from // assets.openFd(name).getParcelFileDescriptor() here as the receiver in the reading // process gets a ParcelFileDescriptor pointing at the whole .apk. Instead, we extract // the asset file we want to storage then wrap that in a ParcelFileDescriptor. File distroFile = null; try { distroFile = File.createTempFile("distro", null, getContext().getFilesDir()); AssetManager assets = getContext().getAssets(); try (InputStream is = assets.open(TimeZoneDistro.FILE_NAME, ACCESS_STREAMING); FileOutputStream fos = new FileOutputStream(distroFile, false /* append */)) { copy(is, fos); } return ParcelFileDescriptor.open(distroFile, ParcelFileDescriptor.MODE_READ_ONLY); } catch (IOException e) { throw new RuntimeException("Unable to copy distro asset file", e); } finally { if (distroFile != null) { // Even if we have an open file descriptor pointing at the file it should be safe to // delete because of normal Unix file behavior. Deleting here avoids leaking any // storage. distroFile.delete(); } } } @Override public String getType(@NonNull Uri uri) { return null; } @Override public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { throw new UnsupportedOperationException(); } @Override public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { throw new UnsupportedOperationException(); } @Override public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) { throw new UnsupportedOperationException(); } private static String getMandatoryMetaDataString(Bundle metaData, String key) { if (!metaData.containsKey(key)) { throw new SecurityException("No metadata with key " + key + " found."); } return metaData.getString(key); } /** * Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed. */ private static void copy(InputStream in, OutputStream out) throws IOException { byte[] buffer = new byte[8192]; int c; while ((c = in.read(buffer)) != -1) { out.write(buffer, 0, c); } } }