diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 7019e72244f..df87bf5fba2 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -98,6 +98,7 @@ + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 66948541c37..ef01841f727 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -11314,4 +11314,11 @@ GPU + + DSU Loader + + Load a Dyanmic System Update Image + + Loading... + diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml index 65a454eff12..ece9822a80f 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" /> + + diff --git a/src/com/android/settings/development/DSULoader.java b/src/com/android/settings/development/DSULoader.java new file mode 100644 index 00000000000..33458ea73d9 --- /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 mDSUList = new ArrayList(); + private ArrayAdapter 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 { + 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 4342c5840c3..7fc898bf452 100644 --- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java +++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java @@ -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)); diff --git a/src/com/android/settings/development/SelectDSUPreferenceController.java b/src/com/android/settings/development/SelectDSUPreferenceController.java new file mode 100644 index 00000000000..72e84472a8f --- /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 00000000000..0aeafc7c7bd --- /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(); + } +}