1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.development; 18 19 import android.app.ListActivity; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.net.Uri; 23 import android.os.Bundle; 24 import android.os.SystemProperties; 25 import android.text.TextUtils; 26 import android.util.Slog; 27 import android.view.LayoutInflater; 28 import android.view.View; 29 import android.view.View.MeasureSpec; 30 import android.view.ViewGroup; 31 import android.widget.ArrayAdapter; 32 import android.widget.ListView; 33 34 import com.android.settings.R; 35 36 import org.json.JSONArray; 37 import org.json.JSONException; 38 import org.json.JSONObject; 39 40 import java.io.BufferedInputStream; 41 import java.io.IOException; 42 import java.io.InputStream; 43 import java.net.MalformedURLException; 44 import java.net.URL; 45 import java.text.ParseException; 46 import java.text.SimpleDateFormat; 47 import java.util.ArrayList; 48 import java.util.Arrays; 49 import java.util.Date; 50 import java.util.List; 51 52 import javax.net.ssl.HttpsURLConnection; 53 54 /** 55 * DSU Loader is a front-end that offers developers the ability to boot into GSI with one-click. It 56 * also offers the flexibility to overwrite the default setting and load OEMs owned images. 57 */ 58 public class DSULoader extends ListActivity { 59 private static final int Q_VNDK_BASE = 28; 60 private static final int Q_OS_BASE = 10; 61 62 private static final boolean DEBUG = false; 63 private static final String TAG = "DSULOADER"; 64 private static final String PROPERTY_KEY_CPU = "ro.product.cpu.abi"; 65 private static final String PROPERTY_KEY_OS = "ro.system.build.version.release"; 66 private static final String PROPERTY_KEY_VNDK = "ro.vndk.version"; 67 private static final String PROPERTY_KEY_LIST = 68 "persist.sys.fflag.override.settings_dynamic_system.list"; 69 private static final String PROPERTY_KEY_SPL = "ro.build.version.security_patch"; 70 private static final String DSU_LIST = 71 "https://dl.google.com/developers/android/gsi/gsi-src.json"; 72 73 private static final int TIMEOUT_MS = 10 * 1000; 74 private List<Object> mDSUList = new ArrayList<Object>(); 75 private ArrayAdapter<Object> mAdapter; 76 readAll(InputStream in)77 private static String readAll(InputStream in) throws IOException { 78 int n; 79 StringBuilder list = new StringBuilder(); 80 byte[] bytes = new byte[4096]; 81 while ((n = in.read(bytes, 0, 4096)) != -1) { 82 list.append(new String(Arrays.copyOf(bytes, n))); 83 } 84 return list.toString(); 85 } 86 readAll(URL url)87 private static String readAll(URL url) throws IOException { 88 InputStream in = null; 89 HttpsURLConnection connection = null; 90 Slog.i(TAG, "fetch " + url.toString()); 91 try { 92 connection = (HttpsURLConnection) url.openConnection(); 93 connection.setReadTimeout(TIMEOUT_MS); 94 connection.setConnectTimeout(TIMEOUT_MS); 95 connection.setRequestMethod("GET"); 96 connection.setDoInput(true); 97 connection.connect(); 98 int responseCode = connection.getResponseCode(); 99 if (connection.getResponseCode() != HttpsURLConnection.HTTP_OK) { 100 throw new IOException("HTTP error code: " + responseCode); 101 } 102 in = new BufferedInputStream(connection.getInputStream()); 103 return readAll(in); 104 } catch (Exception e) { 105 throw e; 106 } finally { 107 try { 108 if (in != null) { 109 in.close(); 110 in = null; 111 } 112 } catch (IOException e) { 113 // ignore 114 } 115 if (connection != null) { 116 connection.disconnect(); 117 connection = null; 118 } 119 } 120 } 121 resizeListView()122 private void resizeListView() { 123 ListView view = getListView(); 124 View item_view = mAdapter.getView(0, null, view); 125 item_view.measure( 126 MeasureSpec.makeMeasureSpec(view.getWidth(), MeasureSpec.EXACTLY), 127 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 128 ViewGroup.LayoutParams params = view.getLayoutParams(); 129 params.height = item_view.getMeasuredHeight() * (mAdapter.getCount() + 1); 130 Slog.e(TAG, "resizeListView height=" + params.height); 131 view.setLayoutParams(params); 132 view.requestLayout(); 133 } 134 135 // Fetcher fetches mDSUList in backgroud 136 private class Fetcher implements Runnable { 137 private URL mDsuList; 138 Fetcher(URL dsuList)139 Fetcher(URL dsuList) { 140 mDsuList = dsuList; 141 } 142 fetch(URL url)143 private void fetch(URL url) 144 throws IOException, JSONException, MalformedURLException, ParseException { 145 String content = readAll(url); 146 JSONObject jsn = new JSONObject(content); 147 // The include primitive is like below 148 // "include": [ 149 // "https:/...json", 150 // ... 151 // ] 152 if (jsn.has("include")) { 153 JSONArray include = jsn.getJSONArray("include"); 154 int len = include.length(); 155 for (int i = 0; i < len; i++) { 156 if (include.isNull(i)) { 157 continue; 158 } 159 fetch(new URL(include.getString(i))); 160 } 161 } 162 // "images":[ 163 // { 164 // "name":"...", 165 // "os_version":"10", 166 // "cpu_abi":"...", 167 // "details":"...", 168 // "vndk":[], 169 // "spl":"...", 170 // "pubkey":"", 171 // "uri":"https://...zip" 172 // }, 173 // ... 174 // ] 175 if (jsn.has("images")) { 176 JSONArray images = jsn.getJSONArray("images"); 177 int len = images.length(); 178 for (int i = 0; i < len; i++) { 179 DSUPackage dsu = new DSUPackage(images.getJSONObject(i)); 180 if (dsu.isSupported()) { 181 mDSUList.add(dsu); 182 } 183 } 184 } 185 } 186 run()187 public void run() { 188 try { 189 fetch(mDsuList); 190 if (mDSUList.size() == 0) { 191 mDSUList.add(0, "No DSU available for this device"); 192 } 193 } catch (IOException e) { 194 Slog.e(TAG, e.toString()); 195 mDSUList.add(0, "Network Error"); 196 } catch (Exception e) { 197 Slog.e(TAG, e.toString()); 198 mDSUList.add(0, "Metadata Error"); 199 } 200 runOnUiThread( 201 new Runnable() { 202 public void run() { 203 mAdapter.clear(); 204 mAdapter.addAll(mDSUList); 205 resizeListView(); 206 } 207 }); 208 } 209 } 210 211 private class DSUPackage { 212 private static final String NAME = "name"; 213 private static final String DETAILS = "details"; 214 private static final String CPU_ABI = "cpu_abi"; 215 private static final String URI = "uri"; 216 private static final String OS_VERSION = "os_version"; 217 private static final String VNDK = "vndk"; 218 private static final String PUBKEY = "pubkey"; 219 private static final String SPL = "spl"; 220 private static final String SPL_FORMAT = "yyyy-MM-dd"; 221 private static final String TOS = "tos"; 222 223 String mName = null; 224 String mDetails = null; 225 String mCpuAbi = null; 226 int mOsVersion = -1; 227 int[] mVndk = null; 228 String mPubKey = ""; 229 Date mSPL = null; 230 URL mTosUrl = null; 231 URL mUri; 232 DSUPackage(JSONObject jsn)233 DSUPackage(JSONObject jsn) throws JSONException, MalformedURLException, ParseException { 234 Slog.i(TAG, "DSUPackage: " + jsn.toString()); 235 mName = jsn.getString(NAME); 236 mDetails = jsn.getString(DETAILS); 237 mCpuAbi = jsn.getString(CPU_ABI); 238 mUri = new URL(jsn.getString(URI)); 239 if (jsn.has(OS_VERSION)) { 240 mOsVersion = dessertNumber(jsn.getString(OS_VERSION), Q_OS_BASE); 241 } 242 if (jsn.has(VNDK)) { 243 JSONArray vndks = jsn.getJSONArray(VNDK); 244 mVndk = new int[vndks.length()]; 245 for (int i = 0; i < vndks.length(); i++) { 246 mVndk[i] = vndks.getInt(i); 247 } 248 } 249 if (jsn.has(PUBKEY)) { 250 mPubKey = jsn.getString(PUBKEY); 251 } 252 if (jsn.has(TOS)) { 253 mTosUrl = new URL(jsn.getString(TOS)); 254 } 255 if (jsn.has(SPL)) { 256 mSPL = new SimpleDateFormat(SPL_FORMAT).parse(jsn.getString(SPL)); 257 } 258 } 259 dessertNumber(String s, int base)260 int dessertNumber(String s, int base) { 261 if (s == null || s.isEmpty()) { 262 return -1; 263 } 264 if (Character.isDigit(s.charAt(0))) { 265 return Integer.parseInt(s); 266 } else { 267 s = s.toUpperCase(); 268 return ((int) s.charAt(0) - (int) 'Q') + base; 269 } 270 } 271 getDeviceVndk()272 int getDeviceVndk() { 273 if (DEBUG) { 274 return Q_VNDK_BASE; 275 } 276 return dessertNumber(SystemProperties.get(PROPERTY_KEY_VNDK), Q_VNDK_BASE); 277 } 278 getDeviceOs()279 int getDeviceOs() { 280 if (DEBUG) { 281 return Q_OS_BASE; 282 } 283 return dessertNumber(SystemProperties.get(PROPERTY_KEY_OS), Q_OS_BASE); 284 } 285 getDeviceCpu()286 String getDeviceCpu() { 287 String cpu = SystemProperties.get(PROPERTY_KEY_CPU); 288 cpu = cpu.toLowerCase(); 289 if (cpu.startsWith("aarch64")) { 290 cpu = "arm64-v8a"; 291 } 292 return cpu; 293 } 294 getDeviceSPL()295 Date getDeviceSPL() { 296 String spl = SystemProperties.get(PROPERTY_KEY_SPL); 297 if (TextUtils.isEmpty(spl)) { 298 return null; 299 } 300 try { 301 return new SimpleDateFormat(SPL_FORMAT).parse(spl); 302 } catch (ParseException e) { 303 return null; 304 } 305 } 306 isSupported()307 boolean isSupported() { 308 boolean supported = true; 309 String cpu = getDeviceCpu(); 310 if (!mCpuAbi.equals(cpu)) { 311 Slog.i(TAG, mCpuAbi + " != " + cpu); 312 supported = false; 313 } 314 if (mOsVersion > 0) { 315 int os = getDeviceOs(); 316 if (os < 0) { 317 Slog.i(TAG, "Failed to getDeviceOs"); 318 supported = false; 319 } else if (mOsVersion < os) { 320 Slog.i(TAG, mOsVersion + " < " + os); 321 supported = false; 322 } 323 } 324 if (mVndk != null) { 325 int vndk = getDeviceVndk(); 326 if (vndk < 0) { 327 Slog.i(TAG, "Failed to getDeviceVndk"); 328 supported = false; 329 } else { 330 boolean found_vndk = false; 331 for (int i = 0; i < mVndk.length; i++) { 332 if (mVndk[i] == vndk) { 333 found_vndk = true; 334 break; 335 } 336 } 337 if (!found_vndk) { 338 Slog.i(TAG, "vndk:" + vndk + " not found"); 339 supported = false; 340 } 341 } 342 } 343 if (mSPL != null) { 344 Date spl = getDeviceSPL(); 345 if (spl == null) { 346 Slog.i(TAG, "Failed to getDeviceSPL"); 347 supported = false; 348 } else if (spl.getTime() > mSPL.getTime()) { 349 Slog.i(TAG, "Device SPL:" + spl.toString() + " > " + mSPL.toString()); 350 supported = false; 351 } 352 } 353 Slog.i(TAG, mName + " isSupported " + supported); 354 return supported; 355 } 356 } 357 358 @Override onCreate(Bundle icicle)359 protected void onCreate(Bundle icicle) { 360 super.onCreate(icicle); 361 String dsuList = SystemProperties.get(PROPERTY_KEY_LIST); 362 Slog.e(TAG, "Try to get DSU list from: " + PROPERTY_KEY_LIST); 363 if (dsuList == null || dsuList.isEmpty()) { 364 dsuList = DSU_LIST; 365 } 366 Slog.e(TAG, "DSU list: " + dsuList); 367 URL url = null; 368 try { 369 url = new URL(dsuList); 370 } catch (MalformedURLException e) { 371 Slog.e(TAG, e.toString()); 372 return; 373 } 374 mAdapter = new DSUPackageListAdapter(this); 375 setListAdapter(mAdapter); 376 mAdapter.add(getResources().getString(R.string.dsu_loader_loading)); 377 new Thread(new Fetcher(url)).start(); 378 } 379 380 @Override onListItemClick(ListView l, View v, int position, long id)381 protected void onListItemClick(ListView l, View v, int position, long id) { 382 Object selected = mAdapter.getItem(position); 383 if (selected instanceof DSUPackage) { 384 DSUPackage dsu = (DSUPackage) selected; 385 mAdapter.clear(); 386 mAdapter.add(getResources().getString(R.string.dsu_loader_loading)); 387 new Thread(new Runnable() { 388 public void run() { 389 String termsOfService = ""; 390 if (dsu.mTosUrl != null) { 391 try { 392 termsOfService = readAll(dsu.mTosUrl); 393 } catch (IOException e) { 394 Slog.e(TAG, e.toString()); 395 } 396 } 397 Intent intent = new Intent(DSULoader.this, DSUTermsOfServiceActivity.class); 398 intent.putExtra(DSUTermsOfServiceActivity.KEY_TOS, termsOfService); 399 intent.setData(Uri.parse(dsu.mUri.toString())); 400 intent.putExtra("KEY_PUBKEY", dsu.mPubKey); 401 startActivity(intent); 402 } 403 }).start(); 404 } 405 finish(); 406 } 407 408 private class DSUPackageListAdapter extends ArrayAdapter<Object> { 409 private final LayoutInflater mInflater; 410 DSUPackageListAdapter(Context context)411 DSUPackageListAdapter(Context context) { 412 super(context, 0); 413 mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 414 } 415 416 @Override getView(int position, View convertView, ViewGroup parent)417 public View getView(int position, View convertView, ViewGroup parent) { 418 AppViewHolder holder = AppViewHolder.createOrRecycle(mInflater, convertView); 419 convertView = holder.rootView; 420 Object item = getItem(position); 421 if (item instanceof DSUPackage) { 422 DSUPackage dsu = (DSUPackage) item; 423 holder.appName.setText(dsu.mName); 424 holder.summary.setText(dsu.mDetails); 425 } else { 426 String msg = (String) item; 427 holder.summary.setText(msg); 428 } 429 holder.appIcon.setImageDrawable(null); 430 holder.disabled.setVisibility(View.GONE); 431 return convertView; 432 } 433 } 434 } 435