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