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();
+ }
+}