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
This commit is contained in:
@@ -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" />
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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">
|
||||
|
368
src/com/android/settings/development/DSULoader.java
Normal file
368
src/com/android/settings/development/DSULoader.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -430,6 +430,7 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra
|
||||
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));
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user