blob: fede67a7766c43ad4d6fba3c9703ba0fd6306474 [file] [log] [blame]
/*
* 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;
}
}
}