Add a GSI/DSU Loader to the developer option.

The Generic System Image (GSI) is a pure Android implementation with unmodified
Android Open Source Project (AOSP) code. The Dynamic System Updates (DSU) is a
Android Q feature that can download a system image and boot into it without the
factory rom being corrupted. This CL adds a DSU Loader which is a friendly
front-end that offers developers the ability to boot into GSI with one-click.
The DSU loader also offers the flexibility to overwrite the default setting
and load OEMs owned images.

Bug: 140090894
Test: click settings->developer option->debug::DSU Loader
    make -j32 RunSettingsRoboTests
Change-Id: Ia2a1b69b52047dd841dedf7f07b07f7ad3e65d46
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 7019e72..df87bf5 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -98,6 +98,7 @@
     <uses-permission android:name="android.permission.MANAGE_SCOPED_ACCESS_DIRECTORY_PERMISSIONS" />
     <uses-permission android:name="android.permission.CAMERA" />
     <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
+    <uses-permission android:name="android.permission.INSTALL_DYNAMIC_SYSTEM" />
 
     <application android:label="@string/settings_label"
             android:icon="@drawable/ic_launcher_settings"
@@ -1945,6 +1946,10 @@
                   android:label="@string/select_application"
                   android:theme="@android:style/Theme.DeviceDefault.Light.Dialog" />
 
+        <activity android:name=".development.DSULoader"
+                  android:label="Select DSU Package"
+                  android:theme="@android:style/Theme.DeviceDefault.Light.Dialog" />
+
         <activity android:name="Settings$WebViewAppPickerActivity"
                   android:label="@string/select_webview_provider_dialog_title" />
 
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 66948541..ef01841 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -11314,4 +11314,11 @@
     <!-- Developer Settings: Search keywords for the Profile HWUI rendering. [CHAR_LIMIT=NONE] -->
     <string name="track_frame_time_keywords">GPU</string>
 
+    <!-- DSU Loader. Do not translate. -->
+    <string name="dsu_loader_title" translatable="false">DSU Loader</string>
+    <!-- DSU Loader Description. Do not translate. -->
+    <string name="dsu_loader_description" translatable="false">Load a Dyanmic System Update Image</string>
+    <!-- DSU Loader Loading. Do not translate. -->
+    <string name="dsu_loader_loading" translatable="false">Loading...</string>
+
 </resources>
diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml
index 65a454e..ece9822 100644
--- a/res/xml/development_settings.xml
+++ b/res/xml/development_settings.xml
@@ -108,6 +108,9 @@
             android:title="@string/ota_disable_automatic_update"
             android:summary="@string/ota_disable_automatic_update_summary" />
 
+        <Preference android:key="dsu_loader"
+                    android:title="@string/dsu_loader_title" />
+
         <Preference
             android:key="demo_mode"
             android:title="@string/demo_mode">
diff --git a/src/com/android/settings/development/DSULoader.java b/src/com/android/settings/development/DSULoader.java
new file mode 100644
index 0000000..33458ea
--- /dev/null
+++ b/src/com/android/settings/development/DSULoader.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2019 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.settings.development;
+
+import android.app.ListActivity;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemProperties;
+import android.util.Slog;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+import com.android.settings.R;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.net.ssl.HttpsURLConnection;
+
+/**
+ * DSU Loader is a front-end that offers developers the ability to boot into GSI with one-click. It
+ * also offers the flexibility to overwrite the default setting and load OEMs owned images.
+ */
+public class DSULoader extends ListActivity {
+    public static final String PROPERTY_KEY_FEATURE_FLAG =
+            "persist.sys.fflag.override.settings_dynamic_system";
+    private static final int Q_VNDK_BASE = 28;
+    private static final int Q_OS_BASE = 10;
+
+    private static final boolean DEBUG = false;
+    private static final String TAG = "DSULOADER";
+    private static final String PROPERTY_KEY_CPU = "ro.product.cpu.abi";
+    private static final String PROPERTY_KEY_OS = "ro.system.build.version.release";
+    private static final String PROPERTY_KEY_VNDK = "ro.vndk.version";
+    private static final String PROPERTY_KEY_LIST = "ro.vendor.dsu.list";
+    private static final String DSU_LIST =
+            "https://dl.google.com/developers/android/gsi/gsi-src.json";
+
+    private static final int TIMEOUT_MS = 10 * 1000;
+    private List<Object> mDSUList = new ArrayList<Object>();
+    private ArrayAdapter<Object> mAdapter;
+
+    private static String readAll(InputStream in) throws IOException {
+        int n;
+        StringBuilder list = new StringBuilder();
+        byte[] bytes = new byte[4096];
+        while ((n = in.read(bytes, 0, 4096)) != -1) {
+            list.append(new String(Arrays.copyOf(bytes, n)));
+        }
+        return list.toString();
+    }
+
+    private static String readAll(URL url) throws IOException {
+        InputStream in = null;
+        HttpsURLConnection connection = null;
+        Slog.i(TAG, "fetch " + url.toString());
+        try {
+            connection = (HttpsURLConnection) url.openConnection();
+            connection.setReadTimeout(TIMEOUT_MS);
+            connection.setConnectTimeout(TIMEOUT_MS);
+            connection.setRequestMethod("GET");
+            connection.setDoInput(true);
+            connection.connect();
+            int responseCode = connection.getResponseCode();
+            if (connection.getResponseCode() != HttpsURLConnection.HTTP_OK) {
+                throw new IOException("HTTP error code: " + responseCode);
+            }
+            in = new BufferedInputStream(connection.getInputStream());
+            return readAll(in);
+        } catch (Exception e) {
+            throw e;
+        } finally {
+            try {
+                if (in != null) {
+                    in.close();
+                    in = null;
+                }
+            } catch (IOException e) {
+                // ignore
+            }
+            if (connection != null) {
+                connection.disconnect();
+                connection = null;
+            }
+        }
+    }
+    // Fetcher fetches mDSUList in backgroud
+    private class Fetcher implements Runnable {
+        private URL mDsuList;
+
+        Fetcher(URL dsuList) {
+            mDsuList = dsuList;
+        }
+
+        private void fetch(URL url) throws IOException, JSONException, MalformedURLException {
+            String content = readAll(url);
+            JSONObject jsn = new JSONObject(content);
+            // The include primitive is like below
+            // "include": [
+            //   "https:/...json",
+            //    ...
+            // ]
+            if (jsn.has("include")) {
+                JSONArray include = jsn.getJSONArray("include");
+                int len = include.length();
+                for (int i = 0; i < len; i++) {
+                    if (include.isNull(i)) {
+                        continue;
+                    }
+                    fetch(new URL(include.getString(i)));
+                }
+            }
+            //  "images":[
+            //    {
+            //      "name":"...",
+            //      "os_version":"10",
+            //      "cpu_abi":"...",
+            //      "details":"...",
+            //      "vndk":[],
+            //      "spl":"...",
+            //      "pubkey":"",
+            //      "uri":"https://...zip"
+            //    },
+            //     ...
+            //  ]
+            if (jsn.has("images")) {
+                JSONArray images = jsn.getJSONArray("images");
+                int len = images.length();
+                for (int i = 0; i < len; i++) {
+                    DSUPackage dsu = new DSUPackage(images.getJSONObject(i));
+                    if (dsu.isSupported()) {
+                        mDSUList.add(dsu);
+                    }
+                }
+            }
+        }
+
+        public void run() {
+            try {
+                fetch(mDsuList);
+            } catch (IOException e) {
+                Slog.e(TAG, e.toString());
+                mDSUList.add(0, "Network Error");
+            } catch (Exception e) {
+                Slog.e(TAG, e.toString());
+                mDSUList.add(0, "Metadata Error");
+            }
+            if (mDSUList.size() == 0) {
+                mDSUList.add(0, "No DSU available for this device");
+            }
+            runOnUiThread(
+                    new Runnable() {
+                        public void run() {
+                            mAdapter.clear();
+                            mAdapter.addAll(mDSUList);
+                        }
+                    });
+        }
+    }
+
+    private class DSUPackage {
+        private static final String NAME = "name";
+        private static final String DETAILS = "details";
+        private static final String CPU_ABI = "cpu_abi";
+        private static final String URI = "uri";
+        private static final String OS_VERSION = "os_version";
+        private static final String VNDK = "vndk";
+        private static final String PUBKEY = "pubkey";
+
+        String mName = null;
+        String mDetails = null;
+        String mCpuAbi = null;
+        int mOsVersion = -1;
+        int[] mVndk = null;
+        String mPubKey = "";
+        URL mUri;
+
+        DSUPackage(JSONObject jsn) throws JSONException, MalformedURLException {
+            Slog.i(TAG, "DSUPackage: " + jsn.toString());
+            mName = jsn.getString(NAME);
+            mDetails = jsn.getString(DETAILS);
+            mCpuAbi = jsn.getString(CPU_ABI);
+            mUri = new URL(jsn.getString(URI));
+            if (jsn.has(OS_VERSION)) {
+                mOsVersion = dessertNumber(jsn.getString(OS_VERSION), Q_OS_BASE);
+            }
+            if (jsn.has(VNDK)) {
+                JSONArray vndks = jsn.getJSONArray(VNDK);
+                mVndk = new int[vndks.length()];
+                for (int i = 0; i < vndks.length(); i++) {
+                    mVndk[i] = vndks.getInt(i);
+                }
+            }
+            if (jsn.has(PUBKEY)) {
+                mPubKey = jsn.getString(PUBKEY);
+            }
+        }
+
+        int dessertNumber(String s, int base) {
+            if (s == null || s.isEmpty()) {
+                return -1;
+            }
+            if (Character.isDigit(s.charAt(0))) {
+                return Integer.parseInt(s);
+            } else {
+                s = s.toUpperCase();
+                return ((int) s.charAt(0) - (int) 'Q') + base;
+            }
+        }
+
+        int getDeviceVndk() {
+            if (DEBUG) {
+                return Q_VNDK_BASE;
+            }
+            return dessertNumber(SystemProperties.get(PROPERTY_KEY_VNDK), Q_VNDK_BASE);
+        }
+
+        int getDeviceOs() {
+            if (DEBUG) {
+                return Q_OS_BASE;
+            }
+            return dessertNumber(SystemProperties.get(PROPERTY_KEY_OS), Q_OS_BASE);
+        }
+
+        String getDeviceCpu() {
+            String cpu = SystemProperties.get(PROPERTY_KEY_CPU);
+            cpu = cpu.toLowerCase();
+            if (cpu.startsWith("aarch64")) {
+                cpu = "arm64-v8a";
+            }
+            return cpu;
+        }
+
+        boolean isSupported() {
+            boolean supported = true;
+            String cpu = getDeviceCpu();
+            if (!mCpuAbi.equals(cpu)) {
+                Slog.i(TAG, mCpuAbi + " != " + cpu);
+                supported = false;
+            }
+            if (mOsVersion > 0) {
+                int os = getDeviceOs();
+                if (os < 0) {
+                    Slog.i(TAG, "Failed to getDeviceOs");
+                    supported = false;
+                } else if (mOsVersion < os) {
+                    Slog.i(TAG, mOsVersion + " < " + os);
+                    supported = false;
+                }
+            }
+            if (mVndk != null) {
+                int vndk = getDeviceVndk();
+                if (vndk < 0) {
+                    Slog.i(TAG, "Failed to getDeviceVndk");
+                    supported = false;
+                } else {
+                    boolean found_vndk = false;
+                    for (int i = 0; i < mVndk.length; i++) {
+                        if (mVndk[i] == vndk) {
+                            found_vndk = true;
+                            break;
+                        }
+                    }
+                    if (!found_vndk) {
+                        Slog.i(TAG, "vndk:" + vndk + " not found");
+                        supported = false;
+                    }
+                }
+            }
+            Slog.i(TAG, mName + " isSupported " + supported);
+            return supported;
+        }
+    }
+
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        SystemProperties.set(PROPERTY_KEY_FEATURE_FLAG, "1");
+        String dsuList = SystemProperties.get(PROPERTY_KEY_LIST);
+        Slog.e(TAG, "Try to get DSU list from: " + PROPERTY_KEY_LIST);
+        if (dsuList == null || dsuList.isEmpty()) {
+            dsuList = DSU_LIST;
+        }
+        Slog.e(TAG, "DSU list: " + dsuList);
+        URL url = null;
+        try {
+            url = new URL(dsuList);
+        } catch (MalformedURLException e) {
+            Slog.e(TAG, e.toString());
+            return;
+        }
+        new Thread(new Fetcher(url)).start();
+        mAdapter = new DSUPackageListAdapter(this);
+        setListAdapter(mAdapter);
+        mAdapter.add(getResources().getString(R.string.dsu_loader_loading));
+    }
+
+    @Override
+    protected void onListItemClick(ListView l, View v, int position, long id) {
+        Object selected = mAdapter.getItem(position);
+        if (selected instanceof DSUPackage) {
+            DSUPackage dsu = (DSUPackage) selected;
+            Intent intent = new Intent();
+            intent.setClassName(
+                    "com.android.dynsystem", "com.android.dynsystem.VerificationActivity");
+            intent.setData(Uri.parse(dsu.mUri.toString()));
+            intent.putExtra("KEY_PUBKEY", dsu.mPubKey);
+            startActivity(intent);
+        }
+        finish();
+    }
+
+    private class DSUPackageListAdapter extends ArrayAdapter<Object> {
+        private final LayoutInflater mInflater;
+
+        DSUPackageListAdapter(Context context) {
+            super(context, 0);
+            mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            AppViewHolder holder = AppViewHolder.createOrRecycle(mInflater, convertView);
+            convertView = holder.rootView;
+            Object item = getItem(position);
+            if (item instanceof DSUPackage) {
+                DSUPackage dsu = (DSUPackage) item;
+                holder.appName.setText(dsu.mName);
+                holder.summary.setText(dsu.mDetails);
+            } else {
+                String msg = (String) item;
+                holder.summary.setText(msg);
+            }
+            holder.appIcon.setImageDrawable(null);
+            holder.disabled.setVisibility(View.GONE);
+            return convertView;
+        }
+    }
+}
diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
index 4342c58..7fc898b 100644
--- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
+++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
@@ -430,6 +430,7 @@
         controllers.add(new WebViewAppPreferenceController(context));
         controllers.add(new CoolColorTemperaturePreferenceController(context));
         controllers.add(new DisableAutomaticUpdatesPreferenceController(context));
+        controllers.add(new SelectDSUPreferenceController(context));
         controllers.add(new AdbPreferenceController(context, fragment));
         controllers.add(new ClearAdbKeysPreferenceController(context, fragment));
         controllers.add(new LocalTerminalPreferenceController(context));
diff --git a/src/com/android/settings/development/SelectDSUPreferenceController.java b/src/com/android/settings/development/SelectDSUPreferenceController.java
new file mode 100644
index 0000000..72e8447
--- /dev/null
+++ b/src/com/android/settings/development/SelectDSUPreferenceController.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2019 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.settings.development;
+
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+import com.android.settingslib.development.DeveloperOptionsPreferenceController;
+
+class SelectDSUPreferenceController extends DeveloperOptionsPreferenceController {
+
+    private static final String DSU_LOADER_KEY = "dsu_loader";
+
+    SelectDSUPreferenceController(Context context) {
+        super(context);
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return DSU_LOADER_KEY;
+    }
+
+    @Override
+    public boolean handlePreferenceTreeClick(Preference preference) {
+        if (DSU_LOADER_KEY.equals(preference.getKey())) {
+            final Intent intent = new Intent(mContext, DSULoader.class);
+            mContext.startActivity(intent);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void updateState(Preference preference) {
+        preference.setSummary(mContext.getResources().getString(R.string.dsu_loader_description));
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/development/SelectDSUPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/SelectDSUPreferenceControllerTest.java
new file mode 100644
index 0000000..0aeafc7
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/development/SelectDSUPreferenceControllerTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2019 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.settings.development;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.SystemProperties;
+
+import androidx.preference.PreferenceScreen;
+import androidx.preference.SwitchPreference;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class SelectDSUPreferenceControllerTest {
+
+    @Mock
+    private SwitchPreference mPreference;
+    @Mock
+    private PreferenceScreen mPreferenceScreen;
+
+    private Context mContext;
+    private SelectDSUPreferenceController mController;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+        mController = new SelectDSUPreferenceController(mContext);
+        when(mPreferenceScreen.findPreference(mController.getPreferenceKey())).thenReturn(
+                mPreference);
+        mController.displayPreference(mPreferenceScreen);
+    }
+
+    @Test
+    public void onPreferenceChanged_settingEnabled_turnOnGpuViewUpdates() {
+        mController.handlePreferenceTreeClick(mPreference);
+        String flag = SystemProperties.get(DSULoader.PROPERTY_KEY_FEATURE_FLAG);
+        assertThat(flag.equals("1")).isTrue();
+    }
+}