blob: c7be72a89e4da96495f4369cfc908774b7801890 [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 libcore.tzdata.prototypedata;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.database.AbstractCursor;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.provider.TimeZoneRulesDataContract;
import android.util.Log;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
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 libcore.io.Streams;
import static android.content.res.AssetManager.ACCESS_STREAMING;
import static android.provider.TimeZoneRulesDataContract.COLUMN_DISTRO_MAJOR_VERSION;
import static android.provider.TimeZoneRulesDataContract.COLUMN_DISTRO_MINOR_VERSION;
import static android.provider.TimeZoneRulesDataContract.COLUMN_OPERATION;
import static android.provider.TimeZoneRulesDataContract.COLUMN_REVISION;
import static android.provider.TimeZoneRulesDataContract.COLUMN_RULES_VERSION;
import static android.provider.TimeZoneRulesDataContract.OPERATION_INSTALL;
/**
* 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 String METADATA_KEY_ASSET = "android.timezoneprovider.DATA_ASSET";
private static final String METADATA_KEY_DISTRO_MAJOR_VERSION
= "android.timezoneprovider.DISTRO_MAJOR_VERSION";
private static final String METADATA_KEY_DISTRO_MINOR_VERSION
= "android.timezoneprovider.DISTRO_MINOR_VERSION";
private static final String METADATA_KEY_RULES_VERSION
= "android.timezoneprovider.RULES_VERSION";
private static final String METADATA_KEY_REVISION
= "android.timezoneprovider.REVISION";
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(COLUMN_OPERATION);
columnNames.add(COLUMN_DISTRO_MAJOR_VERSION);
columnNames.add(COLUMN_DISTRO_MINOR_VERSION);
columnNames.add(COLUMN_RULES_VERSION);
columnNames.add(COLUMN_REVISION);
KNOWN_COLUMN_NAMES = Collections.unmodifiableSet(columnNames);
Map<String, Class<?>> columnTypes = new HashMap<>();
columnTypes.put(COLUMN_OPERATION, String.class);
columnTypes.put(COLUMN_DISTRO_MAJOR_VERSION, Integer.class);
columnTypes.put(COLUMN_DISTRO_MINOR_VERSION, Integer.class);
columnTypes.put(COLUMN_RULES_VERSION, String.class);
columnTypes.put(COLUMN_REVISION, Integer.class);
KNOWN_COLUMN_TYPES = Collections.unmodifiableMap(columnTypes);
}
private Map<String, Object> mColumnData = new HashMap<>();
private String mAssetName;
@Override
public boolean onCreate() {
return true;
}
@Override
public void attachInfo(Context context, ProviderInfo info) {
super.attachInfo(context, info);
// 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");
}
String operation;
try {
operation = getMandatoryMetaDataString(metaData, METADATA_KEY_OPERATION);
mColumnData.put(COLUMN_OPERATION, operation);
} catch (IllegalArgumentException e) {
throw new SecurityException(METADATA_KEY_OPERATION + " meta-data not set.");
}
if (OPERATION_INSTALL.equals(operation)) {
mColumnData.put(
COLUMN_DISTRO_MAJOR_VERSION,
getMandatoryMetaDataInt(metaData, METADATA_KEY_DISTRO_MAJOR_VERSION));
mColumnData.put(
COLUMN_DISTRO_MINOR_VERSION,
getMandatoryMetaDataInt(metaData, METADATA_KEY_DISTRO_MINOR_VERSION));
mColumnData.put(
COLUMN_RULES_VERSION,
getMandatoryMetaDataString(metaData, METADATA_KEY_RULES_VERSION));
mColumnData.put(
COLUMN_REVISION,
getMandatoryMetaDataInt(metaData, METADATA_KEY_REVISION));
// Make sure the asset containing the data to install exists.
String assetName = getMandatoryMetaDataString(metaData, METADATA_KEY_ASSET);
try {
InputStream is = context.getAssets().open(assetName);
// An exception is thrown if the asset does not exist. list(assetName) appears not
// to work with file paths.
is.close();
} catch (IOException e) {
throw new SecurityException("Unable to open asset:" + assetName);
}
mAssetName = assetName;
}
}
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
@Nullable String[] selectionArgs, @Nullable String sortOrder) {
if (!TimeZoneRulesDataContract.OPERATION_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 (!TimeZoneRulesDataContract.DATA_URI.equals(uri)) {
return null;
}
if (mAssetName == null) {
throw new FileNotFoundException();
}
if (!mode.equals("r")) {
throw new SecurityException("Only read-only access supported.");
}
// Extract the asset to a local dir. We do it every time: we don't make assumptions that the
// current copy (if any) is valid.
File localFile = extractAssetToLocalFile();
// Create a read-only ParcelFileDescriptor that can be passed to the caller process.
try {
return ParcelFileDescriptor.open(localFile, ParcelFileDescriptor.MODE_READ_ONLY,
new Handler(Looper.getMainLooper()),
e -> {
if (e != null) {
Log.w(TAG, "Error in OnCloseListener for " + localFile, e);
}
localFile.delete();
});
} catch (IOException e) {
throw new RuntimeException("Unable to open asset file", e);
}
}
private File extractAssetToLocalFile() throws FileNotFoundException {
File extractedFile = new File(getContext().getFilesDir(), "timezone_data.zip");
InputStream is;
try {
is = getContext().getAssets().open(mAssetName, ACCESS_STREAMING);
} catch (FileNotFoundException e) {
throw e;
} catch (IOException e) {
FileNotFoundException fnfe = new FileNotFoundException("Problem reading asset");
fnfe.initCause(e);
throw fnfe;
}
try (InputStream fis = is;
FileOutputStream fos = new FileOutputStream(extractedFile, false /* append */)) {
Streams.copy(fis, fos);
} catch (IOException e) {
throw new RuntimeException("Unable to create asset storage file: " + extractedFile, e);
}
return extractedFile;
}
@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);
}
private static int getMandatoryMetaDataInt(Bundle metaData, String key) {
if (!metaData.containsKey(key)) {
throw new SecurityException("No metadata with key " + key + " found.");
}
return metaData.getInt(key, -1);
}
}