Refactoring of the prototype data app

Switch to using TimeZoneDistro code to read the
distro_version file using TimeZoneDistro rather than
duplicating the information in the manifest.

Fixed the name of the asset containing the distro
for simplicity.

Added proguard.cfg to strip unnecesary code from the
new static library dependency.

Removed dependency on internal Streams class.

Test: Manual testing
Bug: 31008728
Change-Id: I5da659ae8208c5e430078d0b056ba0dc75944c1f
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);
     }
 }