| /* |
| * 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<String> KNOWN_COLUMN_NAMES; |
| private static final Map<String, Class<?>> KNOWN_COLUMN_TYPES; |
| |
| static { |
| Set<String> 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<String, Class<?>> 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<String, Object> 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); |
| } |
| |
| // Confirm 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<String> 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); |
| } |
| } |
| } |