| /* | |
| * 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<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; | |
| } | |
| } | |
| } | |
| 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<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; | |
| } | |
| } | |
| } |