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