Initial check-in of AOSP example/demo/base code

Initial check-in of the AOSP example/demo/base code for the
time zone update data app.

Included are targets for building test versions (beneath the
testing dir) with interesting properties for manual testing.

Many of the files are copied / adapted from files in
libcore/tzdata/prototype_data. Those files will be deleted in a
follow up change.

Test: mmm packages/apps/TimeZoneData
Test: mmm packages/apps/TimeZoneData/testing
Bug: 31008728
Change-Id: Icd18b0ef642e223be20fd106ad90e06ac6911873
diff --git a/src/main/com/android/timezone/data/TimeZoneRulesDataProvider.java b/src/main/com/android/timezone/data/TimeZoneRulesDataProvider.java
new file mode 100644
index 0000000..3f36e86
--- /dev/null
+++ b/src/main/com/android/timezone/data/TimeZoneRulesDataProvider.java
@@ -0,0 +1,330 @@
+/*
+ * 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 com.android.timezone.data;
+
+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;
+import android.content.ContentValues;
+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.ParcelFileDescriptor;
+import android.provider.TimeZoneRulesDataContract;
+
+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;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+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 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 final Map<String, Object> mColumnData = new HashMap<>();
+
+    @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");
+        }
+
+        // Work out what the operation is.
+        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.");
+        }
+
+        // Fill in version information if this is an install operation.
+        if (OPERATION_INSTALL.equals(operation)) {
+            // Extract the version information from the distro.
+            InputStream distroBytesInputStream;
+            try {
+                distroBytesInputStream = context.getAssets().open(TimeZoneDistro.FILE_NAME);
+            } catch (IOException e) {
+                throw new SecurityException(
+                        "Unable to open asset: " + TimeZoneDistro.FILE_NAME, e);
+            }
+            TimeZoneDistro distro = new TimeZoneDistro(distroBytesInputStream);
+            try {
+                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);
+            }
+
+        }
+    }
+
+    @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)) {
+            throw new FileNotFoundException("Unknown URI: " + uri);
+        }
+        if (!"r".equals(mode)) {
+            throw new FileNotFoundException("Only read-only access supported.");
+        }
+
+        // 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 {
+            distroFile = File.createTempFile("distro", null, getContext().getFilesDir());
+
+            AssetManager assets = getContext().getAssets();
+            try (InputStream is = assets.open(TimeZoneDistro.FILE_NAME, ACCESS_STREAMING);
+                 FileOutputStream fos = new FileOutputStream(distroFile, false /* append */)) {
+                copy(is, fos);
+            }
+
+            return ParcelFileDescriptor.open(distroFile, ParcelFileDescriptor.MODE_READ_ONLY);
+        } catch (IOException 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();
+            }
+        }
+    }
+
+    @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);
+    }
+
+    /**
+     * 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);
+        }
+    }
+}