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