aboutsummaryrefslogtreecommitdiff
path: root/tzdata
diff options
context:
space:
mode:
authorTreehugger Robot <treehugger-gerrit@google.com>2017-05-10 16:00:53 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2017-05-10 16:00:55 +0000
commit7a539f9a67930a26e11ff3aa882436494d3b387e (patch)
tree3a227c26041ea9b5c63fc6bf4357f3ce724606fe /tzdata
parent84b1a5950db8e8041ba07fadfe565092103574a4 (diff)
parentadbaf7d3dd38ab45f20a4efe7ee1f18e41029747 (diff)
downloadlibcore-7a539f9a67930a26e11ff3aa882436494d3b387e.tar.gz
Merge "Demo/prototype apps for time zone updates"
Diffstat (limited to 'tzdata')
-rw-r--r--tzdata/prototype_data/Android.mk34
-rw-r--r--tzdata/prototype_data/AndroidManifest.xml46
-rw-r--r--tzdata/prototype_data/assets/test_2030a.zipbin0 -> 223804 bytes
-rw-r--r--tzdata/prototype_data/res/values/strings.xml20
-rw-r--r--tzdata/prototype_data/src/libcore/tzdata/prototypedata/TimeZoneRulesDataProvider.java352
-rw-r--r--tzdata/prototype_updater/Android.mk24
-rw-r--r--tzdata/prototype_updater/AndroidManifest.xml38
-rw-r--r--tzdata/prototype_updater/res/values/strings.xml20
-rw-r--r--tzdata/prototype_updater/src/libcore/tzdata/prototype_updater/RulesCheckReceiver.java276
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
new file mode 100644
index 00000000000..ae274cd6725
--- /dev/null
+++ b/tzdata/prototype_data/assets/test_2030a.zip
Binary files differ
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);
+ }
+ }
+}