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