diff options
author | Treehugger Robot <treehugger-gerrit@google.com> | 2017-05-10 16:00:53 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2017-05-10 16:00:55 +0000 |
commit | 7a539f9a67930a26e11ff3aa882436494d3b387e (patch) | |
tree | 3a227c26041ea9b5c63fc6bf4357f3ce724606fe /tzdata | |
parent | 84b1a5950db8e8041ba07fadfe565092103574a4 (diff) | |
parent | adbaf7d3dd38ab45f20a4efe7ee1f18e41029747 (diff) | |
download | libcore-7a539f9a67930a26e11ff3aa882436494d3b387e.tar.gz |
Merge "Demo/prototype apps for time zone updates"
Diffstat (limited to 'tzdata')
-rw-r--r-- | tzdata/prototype_data/Android.mk | 34 | ||||
-rw-r--r-- | tzdata/prototype_data/AndroidManifest.xml | 46 | ||||
-rw-r--r-- | tzdata/prototype_data/assets/test_2030a.zip | bin | 0 -> 223804 bytes | |||
-rw-r--r-- | tzdata/prototype_data/res/values/strings.xml | 20 | ||||
-rw-r--r-- | tzdata/prototype_data/src/libcore/tzdata/prototypedata/TimeZoneRulesDataProvider.java | 352 | ||||
-rw-r--r-- | tzdata/prototype_updater/Android.mk | 24 | ||||
-rw-r--r-- | tzdata/prototype_updater/AndroidManifest.xml | 38 | ||||
-rw-r--r-- | tzdata/prototype_updater/res/values/strings.xml | 20 | ||||
-rw-r--r-- | tzdata/prototype_updater/src/libcore/tzdata/prototype_updater/RulesCheckReceiver.java | 276 |
9 files changed, 810 insertions, 0 deletions
diff --git a/tzdata/prototype_data/Android.mk b/tzdata/prototype_data/Android.mk new file mode 100644 index 00000000000..568bbaf7c0e --- /dev/null +++ b/tzdata/prototype_data/Android.mk @@ -0,0 +1,34 @@ +# 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. + +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) +LOCAL_MODULE_TAGS := optional +LOCAL_PROGUARD_ENABLED := disabled +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_PACKAGE_NAME := PrototypeTimeZoneDataApp +LOCAL_CERTIFICATE := platform +LOCAL_PRIVILEGED_MODULE := true +include $(BUILD_PACKAGE) + +include $(CLEAR_VARS) +LOCAL_MODULE_TAGS := optional +LOCAL_PROGUARD_ENABLED := disabled +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_PACKAGE_NAME := PrototypeTimeZoneDataApp_data +LOCAL_CERTIFICATE := platform +# Needed to ensure the .apk can be installed. Without it the .apk is missing a .dex. +LOCAL_DEX_PREOPT := false +include $(BUILD_PACKAGE) diff --git a/tzdata/prototype_data/AndroidManifest.xml b/tzdata/prototype_data/AndroidManifest.xml new file mode 100644 index 00000000000..ae0fe6b4db0 --- /dev/null +++ b/tzdata/prototype_data/AndroidManifest.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * 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. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="libcore.tzdata.prototype_data" + android:versionCode="1"> + + <application + android:allowBackup="false" + android:label="@string/app_name"> + + <provider + android:name="libcore.tzdata.prototypedata.TimeZoneRulesDataProvider" + android:authorities="com.android.timezone" + android:grantUriPermissions="true" + android:readPermission="android.permission.UPDATE_TIME_ZONE_RULES" + android:exported="true"> + <meta-data android:name="android.timezoneprovider.OPERATION" + android:value="INSTALL"/> + <meta-data android:name="android.timezoneprovider.DATA_ASSET" + android:value="test_2030a.zip"/> + <meta-data android:name="android.timezoneprovider.DISTRO_MAJOR_VERSION" + android:value="1"/> + <meta-data android:name="android.timezoneprovider.DISTRO_MINOR_VERSION" + android:value="1"/> + <meta-data android:name="android.timezoneprovider.RULES_VERSION" + android:value="2030a"/> + <meta-data android:name="android.timezoneprovider.REVISION" + android:value="1"/> + </provider> + </application> +</manifest> diff --git a/tzdata/prototype_data/assets/test_2030a.zip b/tzdata/prototype_data/assets/test_2030a.zip Binary files differnew file mode 100644 index 00000000000..ae274cd6725 --- /dev/null +++ b/tzdata/prototype_data/assets/test_2030a.zip diff --git a/tzdata/prototype_data/res/values/strings.xml b/tzdata/prototype_data/res/values/strings.xml new file mode 100644 index 00000000000..e25341cda35 --- /dev/null +++ b/tzdata/prototype_data/res/values/strings.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * 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. + --> + +<resources> + <string name="app_name">DataPrototypeApp</string> +</resources> diff --git a/tzdata/prototype_data/src/libcore/tzdata/prototypedata/TimeZoneRulesDataProvider.java b/tzdata/prototype_data/src/libcore/tzdata/prototypedata/TimeZoneRulesDataProvider.java new file mode 100644 index 00000000000..c7be72a89e4 --- /dev/null +++ b/tzdata/prototype_data/src/libcore/tzdata/prototypedata/TimeZoneRulesDataProvider.java @@ -0,0 +1,352 @@ +/* + * 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); + } +} diff --git a/tzdata/prototype_updater/Android.mk b/tzdata/prototype_updater/Android.mk new file mode 100644 index 00000000000..0ddaa450629 --- /dev/null +++ b/tzdata/prototype_updater/Android.mk @@ -0,0 +1,24 @@ +# 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. + +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) +LOCAL_MODULE_TAGS := optional +LOCAL_PROGUARD_ENABLED := disabled +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_PACKAGE_NAME := PrototypeTimeZoneUpdaterApp +LOCAL_CERTIFICATE := platform +LOCAL_PRIVILEGED_MODULE := true +include $(BUILD_PACKAGE) diff --git a/tzdata/prototype_updater/AndroidManifest.xml b/tzdata/prototype_updater/AndroidManifest.xml new file mode 100644 index 00000000000..9b73b0bcc05 --- /dev/null +++ b/tzdata/prototype_updater/AndroidManifest.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * 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. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="libcore.tzdata.prototype_updater" + android:versionCode="1"> + + <uses-permission android:name="android.permission.UPDATE_TIME_ZONE_RULES" /> + + <application + android:allowBackup="false" + android:label="@string/app_name"> + + <receiver android:name=".RulesCheckReceiver" + android:permission="android.permission.TRIGGER_TIME_ZONE_RULES_CHECK" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.timezone.TRIGGER_RULES_UPDATE_CHECK" /> + </intent-filter> + </receiver> + + </application> + +</manifest> diff --git a/tzdata/prototype_updater/res/values/strings.xml b/tzdata/prototype_updater/res/values/strings.xml new file mode 100644 index 00000000000..204ae273899 --- /dev/null +++ b/tzdata/prototype_updater/res/values/strings.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * 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. + --> + +<resources> + <string name="app_name">PrototypeUpdaterApp</string> +</resources> diff --git a/tzdata/prototype_updater/src/libcore/tzdata/prototype_updater/RulesCheckReceiver.java b/tzdata/prototype_updater/src/libcore/tzdata/prototype_updater/RulesCheckReceiver.java new file mode 100644 index 00000000000..9f3a3b82322 --- /dev/null +++ b/tzdata/prototype_updater/src/libcore/tzdata/prototype_updater/RulesCheckReceiver.java @@ -0,0 +1,276 @@ +/* + * 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.prototype_updater; + +import android.app.timezone.Callback; +import android.app.timezone.DistroFormatVersion; +import android.app.timezone.DistroRulesVersion; +import android.app.timezone.RulesManager; +import android.app.timezone.RulesState; +import android.app.timezone.RulesUpdaterContract; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.os.ParcelFileDescriptor; +import android.provider.TimeZoneRulesDataContract; +import android.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import libcore.io.Streams; + +// TODO(nfuller): Prevent multiple broadcasts being handled at once? +// TODO(nfuller): Improve logging +// TODO(nfuller): Make the rules check async? +// TODO(nfuller): Need async generally for SystemService calls from BroadcastReceiver? +public class RulesCheckReceiver extends BroadcastReceiver { + final static String TAG = "RulesCheckReceiver"; + + private RulesManager mRulesManager; + + @Override + public void onReceive(Context context, Intent intent) { + if (!RulesUpdaterContract.ACTION_TRIGGER_RULES_UPDATE_CHECK.equals(intent.getAction())) { + // Unknown. Do nothing. + Log.w(TAG, "Unrecognized intent action received: " + intent + + ", action=" + intent.getAction()); + return; + } + + mRulesManager = (RulesManager) context.getSystemService("timezone"); + + byte[] token = intent.getByteArrayExtra(RulesUpdaterContract.EXTRA_CHECK_TOKEN); + + // Note: We rely on the system server to check that the configured data application is the + // one that exposes the content provider with the well-known authority, and is a privileged + // application as required. It is *not* checked here and it is assumed the updater can trust + // the data application. + + // Obtain the information about what the data app is telling us to do. + String operation = getOperation(context); + if (operation == null) { + // TODO Log + boolean success = true; // No point in retrying. + handleCheckComplete(token, success); + return; + } + switch (operation) { + case TimeZoneRulesDataContract.OPERATION_NO_OP: + // TODO Log + // No-op. Just acknowledge the check. + handleCheckComplete(token, true /* success */); + break; + case TimeZoneRulesDataContract.OPERATION_UNINSTALL: + // TODO Log + handleUninstall(token); + break; + case TimeZoneRulesDataContract.OPERATION_INSTALL: + // TODO Log + DistroVersionInfo distroVersionInfo = getDistroVersionInfo(context); + handleCopyAndInstall(context, token, distroVersionInfo); + break; + default: + // TODO Log + final boolean success = true; // No point in retrying. + handleCheckComplete(token, success); + } + } + + private String getOperation(Context context) { + Cursor cursor = context.getContentResolver() + .query(TimeZoneRulesDataContract.OPERATION_URI, + new String[] { TimeZoneRulesDataContract.COLUMN_OPERATION }, + null /* selection */, null /* selectionArgs */, null /* sortOrder */); + if (cursor == null) { + Log.e(TAG, "getOperation: query returned null"); + return null; + } + if (!cursor.moveToFirst()) { + Log.e(TAG, "getOperation: query returned empty results"); + return null; + } + + try { + return cursor.getString(0); + } catch (Exception e) { + Log.e(TAG, "getOperation: getString() threw an exception", e); + return null; + } + } + + private DistroVersionInfo getDistroVersionInfo(Context context) { + Cursor cursor = context.getContentResolver() + .query(TimeZoneRulesDataContract.OPERATION_URI, + new String[] { + TimeZoneRulesDataContract.COLUMN_DISTRO_MAJOR_VERSION, + TimeZoneRulesDataContract.COLUMN_DISTRO_MINOR_VERSION, + TimeZoneRulesDataContract.COLUMN_RULES_VERSION, + TimeZoneRulesDataContract.COLUMN_REVISION}, + null /* selection */, null /* selectionArgs */, null /* sortOrder */); + if (cursor == null) { + Log.e(TAG, "getDistroVersionInfo: query returned null"); + return null; + } + if (!cursor.moveToFirst()) { + Log.e(TAG, "getDistroVersionInfo: query returned empty results"); + return null; + } + + try { + return new DistroVersionInfo( + cursor.getInt(0), + cursor.getInt(1), + cursor.getString(2), + cursor.getInt(3)); + } catch (Exception e) { + Log.e(TAG, "getDistroVersionInfo: getInt()/getString() threw an exception", e); + return null; + } + } + + private void handleCopyAndInstall(Context context, byte[] checkToken, + DistroVersionInfo distroVersionInfo) { + + // Decide whether to proceed with the install. + RulesState rulesState = mRulesManager.getRulesState(); + if (!(rulesState.isDistroFormatVersionSupported(distroVersionInfo.mDistroFormatVersion) + && rulesState.isSystemVersionOlderThan(distroVersionInfo.mDistroRulesVersion))) { + // Nothing to do. + handleCheckComplete(checkToken, true /* success */); + return; + } + + // Copy the data locally before passing it on....security and whatnot. + // TODO(nfuller): Need to do the copy here? + File file = copyDataToLocalFile(context); + if (file == null) { + // It's possible this may get better if the problem is related to storage space. + boolean success = false; + handleCheckComplete(checkToken, success); + return; + } + handleInstall(checkToken, file); + } + + private static File copyDataToLocalFile(Context context) { + File extractedFile = new File(context.getFilesDir(), "temp.zip"); + ParcelFileDescriptor fileDescriptor; + try { + fileDescriptor = context.getContentResolver().openFileDescriptor( + TimeZoneRulesDataContract.DATA_URI, "r"); + if (fileDescriptor == null) { + throw new FileNotFoundException("ContentProvider returned null"); + } + } catch (FileNotFoundException e) { + Log.e(TAG, "copyDataToLocalFile: Unable to open file descriptor" + + TimeZoneRulesDataContract.DATA_URI, e); + return null; + } + + try (ParcelFileDescriptor pfd = fileDescriptor; + InputStream fis = new FileInputStream(pfd.getFileDescriptor()); + FileOutputStream fos = new FileOutputStream(extractedFile, false /* append */)) { + Streams.copy(fis, fos); + } catch (IOException e) { + Log.e(TAG, "Unable to create asset storage file: " + extractedFile, e); + return null; + } + return extractedFile; + } + + private void handleInstall(final byte[] checkToken, final File contentFile) { + // Convert the distroFile to a ParcelFileDescriptor. + final ParcelFileDescriptor distroFileDescriptor; + try { + distroFileDescriptor = + ParcelFileDescriptor.open(contentFile, ParcelFileDescriptor.MODE_READ_ONLY); + } catch (FileNotFoundException e) { + Log.e(TAG, "Unable to create ParcelFileDescriptor from " + contentFile); + handleCheckComplete(checkToken, false /* success */); + return; + } + + Callback callback = new Callback() { + @Override + public void onFinished(int status) { + Log.i(TAG, "onFinished: Finished install: " + status); + + // TODO(nfuller): Can this be closed sooner? + try { + distroFileDescriptor.close(); + } catch (IOException e) { + Log.e(TAG, "Unable to close ParcelFileDescriptor for " + contentFile, e); + } finally { + // Delete the file we no longer need. + contentFile.delete(); + } + } + }; + + try { + int requestStatus = + mRulesManager.requestInstall(distroFileDescriptor, checkToken, callback); + Log.i(TAG, "handleInstall: Request sent:" + requestStatus); + } catch (Exception e) { + Log.e(TAG, "handleInstall: Error", e); + } + } + + private void handleUninstall(byte[] checkToken) { + Callback callback = new Callback() { + @Override + public void onFinished(int status) { + Log.i(TAG, "onFinished: Finished uninstall: " + status); + } + }; + + try { + int requestStatus = + mRulesManager.requestUninstall(checkToken, callback); + Log.i(TAG, "handleUninstall: Request sent" + requestStatus); + } catch (Exception e) { + Log.e(TAG, "handleUninstall: Error", e); + } + } + + private void handleCheckComplete(final byte[] token, final boolean success) { + try { + mRulesManager.requestNothing(token, success); + Log.i(TAG, "doInBackground: Called checkComplete: token=" + + Arrays.toString(token) + ", success=" + success); + } catch (Exception e) { + Log.e(TAG, "doInBackground: Error calling checkComplete()", e); + } + } + + private static class DistroVersionInfo { + + final DistroFormatVersion mDistroFormatVersion; + final DistroRulesVersion mDistroRulesVersion; + + DistroVersionInfo(int distroMajorVersion, int distroMinorVersion, + String rulesVersion, int revision) { + mDistroFormatVersion = new DistroFormatVersion(distroMajorVersion, distroMinorVersion); + mDistroRulesVersion = new DistroRulesVersion(rulesVersion, revision); + } + } +} |