Merge "Refactoring of the prototype data app"
diff --git a/tzdata/prototype_data/Android.mk b/tzdata/prototype_data/Android.mk
index 568bbaf..c618da3 100644
--- a/tzdata/prototype_data/Android.mk
+++ b/tzdata/prototype_data/Android.mk
@@ -16,19 +16,21 @@
 
 include $(CLEAR_VARS)
 LOCAL_MODULE_TAGS := optional
-LOCAL_PROGUARD_ENABLED := disabled
+LOCAL_PROGUARD_FLAG_FILES := proguard.cfg
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 LOCAL_PACKAGE_NAME := PrototypeTimeZoneDataApp
 LOCAL_CERTIFICATE := platform
 LOCAL_PRIVILEGED_MODULE := true
+LOCAL_STATIC_JAVA_LIBRARIES := time_zone_distro
 include $(BUILD_PACKAGE)
 
 include $(CLEAR_VARS)
 LOCAL_MODULE_TAGS := optional
-LOCAL_PROGUARD_ENABLED := disabled
+LOCAL_PROGUARD_FLAG_FILES := proguard.cfg
 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
+LOCAL_STATIC_JAVA_LIBRARIES := time_zone_distro
 include $(BUILD_PACKAGE)
diff --git a/tzdata/prototype_data/AndroidManifest.xml b/tzdata/prototype_data/AndroidManifest.xml
index ae0fe6b..13b60e5 100644
--- a/tzdata/prototype_data/AndroidManifest.xml
+++ b/tzdata/prototype_data/AndroidManifest.xml
@@ -17,7 +17,7 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="libcore.tzdata.prototype_data"
-          android:versionCode="1">
+          android:versionCode="4">
 
     <application
         android:allowBackup="false"
@@ -31,16 +31,6 @@
                 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/distro.zip
similarity index 99%
rename from tzdata/prototype_data/assets/test_2030a.zip
rename to tzdata/prototype_data/assets/distro.zip
index ae274cd..1287f1a 100644
--- a/tzdata/prototype_data/assets/test_2030a.zip
+++ b/tzdata/prototype_data/assets/distro.zip
Binary files differ
diff --git a/tzdata/prototype_data/proguard.cfg b/tzdata/prototype_data/proguard.cfg
new file mode 100644
index 0000000..49cccc4
--- /dev/null
+++ b/tzdata/prototype_data/proguard.cfg
@@ -0,0 +1,3 @@
+-keep class libcore.tzdata.prototypedata.** { *; }
+-dontobfuscate
+-verbose
diff --git a/tzdata/prototype_data/src/libcore/tzdata/prototypedata/TimeZoneRulesDataProvider.java b/tzdata/prototype_data/src/libcore/tzdata/prototypedata/TimeZoneRulesDataProvider.java
index c7be72a..49758ef 100644
--- a/tzdata/prototype_data/src/libcore/tzdata/prototypedata/TimeZoneRulesDataProvider.java
+++ b/tzdata/prototype_data/src/libcore/tzdata/prototypedata/TimeZoneRulesDataProvider.java
@@ -16,6 +16,10 @@
 
 package libcore.tzdata.prototypedata;
 
+import com.android.timezone.distro.DistroException;
+import com.android.timezone.distro.DistroVersion;
+import com.android.timezone.distro.TimeZoneDistro;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.ContentProvider;
@@ -23,21 +27,20 @@
 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.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.io.OutputStream;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
@@ -45,7 +48,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import libcore.io.Streams;
+import java.util.function.Supplier;
 
 import static android.content.res.AssetManager.ACCESS_STREAMING;
 import static android.provider.TimeZoneRulesDataContract.COLUMN_DISTRO_MAJOR_VERSION;
@@ -64,18 +67,10 @@
     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);
@@ -94,8 +89,7 @@
         KNOWN_COLUMN_TYPES = Collections.unmodifiableMap(columnTypes);
     }
 
-    private Map<String, Object> mColumnData = new HashMap<>();
-    private String mAssetName;
+    private final Map<String, Object> mColumnData = new HashMap<>();
 
     @Override
     public boolean onCreate() {
@@ -138,6 +132,7 @@
             throw new SecurityException("meta-data must be set");
         }
 
+        // Work out what the operation is.
         String operation;
         try {
             operation = getMandatoryMetaDataString(metaData, METADATA_KEY_OPERATION);
@@ -145,31 +140,29 @@
         } 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);
+        // Fill in version information if this is an install operation.
+        if (OPERATION_INSTALL.equals(operation)) {
+            // Extract the version information from the distro.
+            Supplier<InputStream> distroBytesSupplier = () -> {
+                try {
+                    return context.getAssets().open(TimeZoneDistro.FILE_NAME);
+                } catch (IOException e) {
+                    throw new SecurityException(
+                            "Unable to open asset: " + TimeZoneDistro.FILE_NAME, e);
+                }
+            };
+            TimeZoneDistro distro = new TimeZoneDistro(distroBytesSupplier);
             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);
+                DistroVersion distroVersion = distro.getDistroVersion();
+                mColumnData.put(COLUMN_DISTRO_MAJOR_VERSION, distroVersion.formatMajorVersion);
+                mColumnData.put(COLUMN_DISTRO_MINOR_VERSION, distroVersion.formatMinorVersion);
+                mColumnData.put(COLUMN_RULES_VERSION, distroVersion.rulesVersion);
+                mColumnData.put(COLUMN_REVISION, distroVersion.revision);
+            } catch (IOException | DistroException e) {
+                throw new SecurityException("Invalid asset: " + TimeZoneDistro.FILE_NAME, e);
             }
-            mAssetName = assetName;
+
         }
     }
 
@@ -264,54 +257,37 @@
     public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
             throws FileNotFoundException {
         if (!TimeZoneRulesDataContract.DATA_URI.equals(uri)) {
-            return null;
+            throw new FileNotFoundException("Unknown URI: " + uri);
         }
-        if (mAssetName == null) {
-            throw new FileNotFoundException();
-        }
-        if (!mode.equals("r")) {
-            throw new SecurityException("Only read-only access supported.");
+        if (!"r".equals(mode)) {
+            throw new FileNotFoundException("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.
+        // 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 {
-            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);
-        }
-    }
+            distroFile = File.createTempFile("distro", null, getContext().getFilesDir());
 
-    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;
-        }
+            AssetManager assets = getContext().getAssets();
+            try (InputStream is = assets.open(TimeZoneDistro.FILE_NAME, ACCESS_STREAMING);
+                 FileOutputStream fos = new FileOutputStream(distroFile, false /* append */)) {
+                copy(is, fos);
+            }
 
-        try (InputStream fis = is;
-                FileOutputStream fos = new FileOutputStream(extractedFile, false /* append */)) {
-            Streams.copy(fis, fos);
+            return ParcelFileDescriptor.open(distroFile, ParcelFileDescriptor.MODE_READ_ONLY);
         } catch (IOException e) {
-            throw new RuntimeException("Unable to create asset storage file: " + extractedFile, 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();
+            }
         }
-        return extractedFile;
     }
 
     @Override
@@ -343,10 +319,14 @@
         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.");
+    /**
+     * 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);
         }
-        return metaData.getInt(key, -1);
     }
 }