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 package android.os.image;
17 
18 import android.annotation.BytesLong;
19 import android.annotation.CallbackExecutor;
20 import android.annotation.IntDef;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.annotation.RequiresPermission;
24 import android.annotation.SystemApi;
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.ServiceConnection;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.os.IBinder;
33 import android.os.Looper;
34 import android.os.Message;
35 import android.os.Messenger;
36 import android.os.ParcelableException;
37 import android.os.RemoteException;
38 import android.util.Slog;
39 
40 import java.lang.annotation.Retention;
41 import java.lang.annotation.RetentionPolicy;
42 import java.lang.ref.WeakReference;
43 import java.util.concurrent.Executor;
44 
45 /**
46  * <p>This class contains methods and constants used to start a {@code DynamicSystem} installation,
47  * and a listener for status updates.</p>
48  *
49  * <p>{@code DynamicSystem} allows users to run certified system images in a non destructive manner
50  * without needing to prior OEM unlock. It creates a temporary system partition to install the new
51  * system image, and a temporary data partition for the newly installed system to run with.</p>
52  *
53  * After the installation is completed, the device will be running in the new system on next the
54  * reboot. Then, when the user reboots the device again, it will leave {@code DynamicSystem} and go
55  * back to the original system. While running in {@code DynamicSystem}, persistent storage for
56  * factory reset protection (FRP) remains unchanged. Since the user is running the new system with
57  * a temporarily created data partition, their original user data are kept unchanged.</p>
58  *
59  * <p>With {@link #setOnStatusChangedListener}, API users can register an
60  * {@link #OnStatusChangedListener} to get status updates and their causes when the installation is
61  * started, stopped, or cancelled. It also sends progress updates during the installation. With
62  * {@link #start}, API users can start an installation with the {@link Uri} to a unsparsed and
63  * gzipped system image. The {@link Uri} can be a web URL or a content Uri to a local path.</p>
64  *
65  * @hide
66  */
67 @SystemApi
68 public class DynamicSystemClient {
69     private static final String TAG = "DynamicSystemClient";
70 
71     /** @hide */
72     @IntDef(prefix = { "STATUS_" }, value = {
73             STATUS_UNKNOWN,
74             STATUS_NOT_STARTED,
75             STATUS_IN_PROGRESS,
76             STATUS_READY,
77             STATUS_IN_USE,
78     })
79     @Retention(RetentionPolicy.SOURCE)
80     public @interface InstallationStatus {}
81 
82     /** @hide */
83     @IntDef(prefix = { "CAUSE_" }, value = {
84             CAUSE_NOT_SPECIFIED,
85             CAUSE_INSTALL_COMPLETED,
86             CAUSE_INSTALL_CANCELLED,
87             CAUSE_ERROR_IO,
88             CAUSE_ERROR_INVALID_URL,
89             CAUSE_ERROR_IPC,
90             CAUSE_ERROR_EXCEPTION,
91     })
92     @Retention(RetentionPolicy.SOURCE)
93     public @interface StatusChangedCause {}
94 
95     /** Listener for installation status updates. */
96     public interface OnStatusChangedListener {
97         /**
98          * This callback is called when installation status is changed, and when the
99          * client is {@link #bind} to {@code DynamicSystem} installation service.
100          *
101          * @param status status code, also defined in {@code DynamicSystemClient}.
102          * @param cause cause code, also defined in {@code DynamicSystemClient}.
103          * @param progress number of bytes installed.
104          * @param detail additional detail about the error if available, otherwise null.
105          */
onStatusChanged(@nstallationStatus int status, @StatusChangedCause int cause, @BytesLong long progress, @Nullable Throwable detail)106         void onStatusChanged(@InstallationStatus int status, @StatusChangedCause int cause,
107                 @BytesLong long progress, @Nullable Throwable detail);
108     }
109 
110     /*
111      * Status codes
112      */
113     /** We are bound to installation service, but failed to get its status */
114     public static final int STATUS_UNKNOWN = 0;
115 
116     /** Installation is not started yet. */
117     public static final int STATUS_NOT_STARTED = 1;
118 
119     /** Installation is in progress. */
120     public static final int STATUS_IN_PROGRESS = 2;
121 
122     /** Installation is finished but the user has not launched it. */
123     public static final int STATUS_READY = 3;
124 
125     /** Device is running in {@code DynamicSystem}. */
126     public static final int STATUS_IN_USE = 4;
127 
128     /*
129      * Causes
130      */
131     /** Cause is not specified. This means the status is not changed. */
132     public static final int CAUSE_NOT_SPECIFIED = 0;
133 
134     /** Status changed because installation is completed. */
135     public static final int CAUSE_INSTALL_COMPLETED = 1;
136 
137     /** Status changed because installation is cancelled. */
138     public static final int CAUSE_INSTALL_CANCELLED = 2;
139 
140     /** Installation failed due to {@code IOException}. */
141     public static final int CAUSE_ERROR_IO = 3;
142 
143     /** Installation failed because the image URL source is not supported. */
144     public static final int CAUSE_ERROR_INVALID_URL = 4;
145 
146     /** Installation failed due to IPC error. */
147     public static final int CAUSE_ERROR_IPC = 5;
148 
149     /** Installation failed due to unhandled exception. */
150     public static final int CAUSE_ERROR_EXCEPTION = 6;
151 
152     /*
153      * IPC Messages
154      */
155     /**
156      * Message to register listener.
157      * @hide
158      */
159     public static final int MSG_REGISTER_LISTENER = 1;
160 
161     /**
162      * Message to unregister listener.
163      * @hide
164      */
165     public static final int MSG_UNREGISTER_LISTENER = 2;
166 
167     /**
168      * Message for status updates.
169      * @hide
170      */
171     public static final int MSG_POST_STATUS = 3;
172 
173     /*
174      * Messages keys
175      */
176     /**
177      * Message key, for progress updates.
178      * @hide
179      */
180     public static final String KEY_INSTALLED_SIZE = "KEY_INSTALLED_SIZE";
181 
182     /**
183      * Message key, used when the service is sending exception detail to the client.
184      * @hide
185      */
186     public static final String KEY_EXCEPTION_DETAIL = "KEY_EXCEPTION_DETAIL";
187 
188     /*
189      * Intent Actions
190      */
191     /**
192      * Intent action: start installation.
193      * @hide
194      */
195     public static final String ACTION_START_INSTALL =
196             "android.os.image.action.START_INSTALL";
197 
198     /**
199      * Intent action: notify user if we are currently running in {@code DynamicSystem}.
200      * @hide
201      */
202     public static final String ACTION_NOTIFY_IF_IN_USE =
203             "android.os.image.action.NOTIFY_IF_IN_USE";
204 
205     /**
206      * Intent action: hide notifications about the status of {@code DynamicSystem}.
207      * @hide
208      */
209     public static final String ACTION_HIDE_NOTIFICATION =
210             "android.os.image.action.HIDE_NOTIFICATION";
211 
212     /**
213      * Intent action: notify the service to post a status update when keyguard is dismissed.
214      * @hide
215      */
216     public static final String ACTION_NOTIFY_KEYGUARD_DISMISSED =
217             "android.os.image.action.NOTIFY_KEYGUARD_DISMISSED";
218 
219     /*
220      * Intent Keys
221      */
222     /**
223      * Intent key: Size of the system image, in bytes.
224      * @hide
225      */
226     public static final String KEY_SYSTEM_SIZE = "KEY_SYSTEM_SIZE";
227 
228     /**
229      * Intent key: Number of bytes to reserve for userdata.
230      * @hide
231      */
232     public static final String KEY_USERDATA_SIZE = "KEY_USERDATA_SIZE";
233 
234     /**
235      * Intent key: Whether to enable DynamicSystem immediately after installation is done.
236      *             Note this will reboot the device automatically.
237      * @hide
238      */
239     public static final String KEY_ENABLE_WHEN_COMPLETED = "KEY_ENABLE_WHEN_COMPLETED";
240 
241     /**
242      * Intent key: Whether to leave DynamicSystem on device reboot.
243      *             False indicates a sticky mode where device stays in DynamicSystem across reboots.
244      * @hide
245      */
246     public static final String KEY_ONE_SHOT = "KEY_ONE_SHOT";
247 
248     /**
249      * Intent key: Whether to use default strings when showing the dialog that prompts
250      *             user for device credentials.
251      *             False indicates using the custom strings provided by {@code DynamicSystem}.
252      * @hide
253      */
254     public static final String KEY_KEYGUARD_USE_DEFAULT_STRINGS =
255             "KEY_KEYGUARD_USE_DEFAULT_STRINGS";
256 
257     private static class IncomingHandler extends Handler {
258         private final WeakReference<DynamicSystemClient> mWeakClient;
259 
IncomingHandler(DynamicSystemClient service)260         IncomingHandler(DynamicSystemClient service) {
261             super(Looper.getMainLooper());
262             mWeakClient = new WeakReference<>(service);
263         }
264 
265         @Override
handleMessage(Message msg)266         public void handleMessage(Message msg) {
267             DynamicSystemClient service = mWeakClient.get();
268 
269             if (service != null) {
270                 service.handleMessage(msg);
271             }
272         }
273     }
274 
275     private class DynSystemServiceConnection implements ServiceConnection {
onServiceConnected(ComponentName className, IBinder service)276         public void onServiceConnected(ComponentName className, IBinder service) {
277             Slog.v(TAG, "onServiceConnected: " + className);
278 
279             mService = new Messenger(service);
280 
281             try {
282                 Message msg = Message.obtain(null, MSG_REGISTER_LISTENER);
283                 msg.replyTo = mMessenger;
284 
285                 mService.send(msg);
286             } catch (RemoteException e) {
287                 Slog.e(TAG, "Unable to get status from installation service");
288                 notifyOnStatusChangedListener(STATUS_UNKNOWN, CAUSE_ERROR_IPC, 0, e);
289             }
290         }
291 
onServiceDisconnected(ComponentName className)292         public void onServiceDisconnected(ComponentName className) {
293             Slog.v(TAG, "onServiceDisconnected: " + className);
294             mService = null;
295         }
296     }
297 
298     private final Context mContext;
299     private final DynSystemServiceConnection mConnection;
300     private final Messenger mMessenger;
301 
302     private boolean mBound;
303     private Executor mExecutor;
304     private OnStatusChangedListener mListener;
305     private Messenger mService;
306 
307     /**
308      * Create a new {@code DynamicSystem} client.
309      *
310      * @param context a {@link Context} will be used to bind the installation service.
311      *
312      * @hide
313      */
314     @SystemApi
DynamicSystemClient(@onNull Context context)315     public DynamicSystemClient(@NonNull Context context) {
316         mContext = context;
317         mConnection = new DynSystemServiceConnection();
318         mMessenger = new Messenger(new IncomingHandler(this));
319     }
320 
321     /**
322      * This method register a listener for status change. The listener is called using
323      * the executor.
324      */
setOnStatusChangedListener( @onNull @allbackExecutor Executor executor, @NonNull OnStatusChangedListener listener)325     public void setOnStatusChangedListener(
326             @NonNull @CallbackExecutor Executor executor,
327             @NonNull OnStatusChangedListener listener) {
328         mListener = listener;
329         mExecutor = executor;
330     }
331 
332     /**
333      * This method register a listener for status change. The listener is called in main
334      * thread.
335      */
setOnStatusChangedListener( @onNull OnStatusChangedListener listener)336     public void setOnStatusChangedListener(
337             @NonNull OnStatusChangedListener listener) {
338         mListener = listener;
339         mExecutor = null;
340     }
341 
notifyOnStatusChangedListener( int status, int cause, long progress, Throwable detail)342     private void notifyOnStatusChangedListener(
343             int status, int cause, long progress, Throwable detail) {
344         if (mListener != null) {
345             if (mExecutor != null) {
346                 mExecutor.execute(
347                         () -> {
348                             mListener.onStatusChanged(status, cause, progress, detail);
349                         });
350             } else {
351                 mListener.onStatusChanged(status, cause, progress, detail);
352             }
353         }
354     }
355 
356     /**
357      * Bind to {@code DynamicSystem} installation service. Binding to the installation service
358      * allows it to send status updates to {@link #OnStatusChangedListener}. It is recommanded
359      * to bind before calling {@link #start} and get status updates.
360      * @hide
361      */
362     @RequiresPermission(android.Manifest.permission.INSTALL_DYNAMIC_SYSTEM)
363     @SystemApi
bind()364     public void bind() {
365         Intent intent = new Intent();
366         intent.setClassName("com.android.dynsystem",
367                 "com.android.dynsystem.DynamicSystemInstallationService");
368 
369         mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
370 
371         mBound = true;
372     }
373 
374     /**
375      * Unbind from {@code DynamicSystem} installation service. Unbinding from the installation
376      * service stops it from sending following status updates.
377      * @hide
378      */
379     @RequiresPermission(android.Manifest.permission.INSTALL_DYNAMIC_SYSTEM)
380     @SystemApi
unbind()381     public void unbind() {
382         if (!mBound) {
383             return;
384         }
385 
386         if (mService != null) {
387             try {
388                 Message msg = Message.obtain(null, MSG_UNREGISTER_LISTENER);
389                 msg.replyTo = mMessenger;
390                 mService.send(msg);
391             } catch (RemoteException e) {
392                 Slog.e(TAG, "Unable to unregister from installation service");
393             }
394         }
395 
396         // Detach our existing connection.
397         mContext.unbindService(mConnection);
398 
399         mBound = false;
400     }
401 
402     /**
403      * Start installing {@code DynamicSystem} from URL with default userdata size.
404      *
405      * Calling this function will first start an Activity to confirm device credential, using
406      * {@link KeyguardManager}. If it's confirmed, the installation service will be started.
407      *
408      * This function doesn't require prior calling {@link #bind}.
409      *
410      * @param systemUrl a network Uri, a file Uri or a content Uri pointing to a system image file.
411      * @param systemSize size of system image.
412      * @hide
413      */
414     @RequiresPermission(android.Manifest.permission.INSTALL_DYNAMIC_SYSTEM)
415     @SystemApi
start(@onNull Uri systemUrl, @BytesLong long systemSize)416     public void start(@NonNull Uri systemUrl, @BytesLong long systemSize) {
417         start(systemUrl, systemSize, 0 /* Use the default userdata size */);
418     }
419 
420     /**
421      * Start installing {@code DynamicSystem} from URL.
422      *
423      * Calling this function will first start an Activity to confirm device credential, using
424      * {@link KeyguardManager}. If it's confirmed, the installation service will be started.
425      *
426      * This function doesn't require prior calling {@link #bind}.
427      *
428      * @param systemUrl a network Uri, a file Uri or a content Uri pointing to a system image file.
429      * @param systemSize size of system image.
430      * @param userdataSize bytes reserved for userdata.
431      */
432     @RequiresPermission(android.Manifest.permission.INSTALL_DYNAMIC_SYSTEM)
start(@onNull Uri systemUrl, @BytesLong long systemSize, @BytesLong long userdataSize)433     public void start(@NonNull Uri systemUrl, @BytesLong long systemSize,
434             @BytesLong long userdataSize) {
435         Intent intent = new Intent();
436 
437         intent.setClassName("com.android.dynsystem",
438                 "com.android.dynsystem.VerificationActivity");
439 
440         intent.setData(systemUrl);
441         intent.setAction(ACTION_START_INSTALL);
442         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
443 
444         intent.putExtra(KEY_SYSTEM_SIZE, systemSize);
445         intent.putExtra(KEY_USERDATA_SIZE, userdataSize);
446 
447         mContext.startActivity(intent);
448     }
449 
handleMessage(Message msg)450     private void handleMessage(Message msg) {
451         switch (msg.what) {
452             case MSG_POST_STATUS:
453                 int status = msg.arg1;
454                 int cause = msg.arg2;
455                 // obj is non-null
456                 Bundle bundle = (Bundle) msg.obj;
457                 long progress = bundle.getLong(KEY_INSTALLED_SIZE);
458                 ParcelableException t = (ParcelableException) bundle.getSerializable(
459                         KEY_EXCEPTION_DETAIL, android.os.ParcelableException.class);
460 
461                 Throwable detail = t == null ? null : t.getCause();
462 
463                 notifyOnStatusChangedListener(status, cause, progress, detail);
464                 break;
465             default:
466                 // do nothing
467 
468         }
469     }
470 }
471