/* * 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.text.TextUtils; import android.util.Slog; import android.view.LayoutInflater; import android.view.View; import android.view.View.MeasureSpec; 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.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; 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 { 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 = "persist.sys.fflag.override.settings_dynamic_system.list"; private static final String PROPERTY_KEY_SPL = "ro.build.version.security_patch"; 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; } } } private void resizeListView() { ListView view = getListView(); View item_view = mAdapter.getView(0, null, view); item_view.measure( MeasureSpec.makeMeasureSpec(view.getWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); ViewGroup.LayoutParams params = view.getLayoutParams(); params.height = item_view.getMeasuredHeight() * (mAdapter.getCount() + 1); Slog.e(TAG, "resizeListView height=" + params.height); view.setLayoutParams(params); view.requestLayout(); } // 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, ParseException { 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); if (mDSUList.size() == 0) { mDSUList.add(0, "No DSU available for this device"); } } 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"); } runOnUiThread( new Runnable() { public void run() { mAdapter.clear(); mAdapter.addAll(mDSUList); resizeListView(); } }); } } 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"; private static final String SPL = "spl"; private static final String SPL_FORMAT = "yyyy-MM-dd"; private static final String TOS = "tos"; String mName = null; String mDetails = null; String mCpuAbi = null; int mOsVersion = -1; int[] mVndk = null; String mPubKey = ""; Date mSPL = null; URL mTosUrl = null; URL mUri; DSUPackage(JSONObject jsn) throws JSONException, MalformedURLException, ParseException { 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); } if (jsn.has(TOS)) { mTosUrl = new URL(jsn.getString(TOS)); } if (jsn.has(SPL)) { mSPL = new SimpleDateFormat(SPL_FORMAT).parse(jsn.getString(SPL)); } } 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; } Date getDeviceSPL() { String spl = SystemProperties.get(PROPERTY_KEY_SPL); if (TextUtils.isEmpty(spl)) { return null; } try { return new SimpleDateFormat(SPL_FORMAT).parse(spl); } catch (ParseException e) { return null; } } 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; } } } if (mSPL != null) { Date spl = getDeviceSPL(); if (spl == null) { Slog.i(TAG, "Failed to getDeviceSPL"); supported = false; } else if (spl.getTime() > mSPL.getTime()) { Slog.i(TAG, "Device SPL:" + spl.toString() + " > " + mSPL.toString()); supported = false; } } Slog.i(TAG, mName + " isSupported " + supported); return supported; } } @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); 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; } mAdapter = new DSUPackageListAdapter(this); setListAdapter(mAdapter); mAdapter.add(getResources().getString(R.string.dsu_loader_loading)); new Thread(new Fetcher(url)).start(); } @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; mAdapter.clear(); mAdapter.add(getResources().getString(R.string.dsu_loader_loading)); new Thread(new Runnable() { public void run() { String termsOfService = ""; if (dsu.mTosUrl != null) { try { termsOfService = readAll(dsu.mTosUrl); } catch (IOException e) { Slog.e(TAG, e.toString()); } } Intent intent = new Intent(DSULoader.this, DSUTermsOfServiceActivity.class); intent.putExtra(DSUTermsOfServiceActivity.KEY_TOS, termsOfService); intent.setData(Uri.parse(dsu.mUri.toString())); intent.putExtra("KEY_PUBKEY", dsu.mPubKey); startActivity(intent); } }).start(); } 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; } } }