blob: 0c1b39c8d0ed86fa87418811a1249c9424dc2611 [file] [log] [blame]
/*
* 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);
}
// 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<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);
}
}
}