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}, persitent 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 Keys
207      */
208     /**
209      * Intent key: Size of the system image, in bytes.
210      * @hide
211      */
212     public static final String KEY_SYSTEM_SIZE = "KEY_SYSTEM_SIZE";
213 
214     /**
215      * Intent key: Number of bytes to reserve for userdata.
216      * @hide
217      */
218     public static final String KEY_USERDATA_SIZE = "KEY_USERDATA_SIZE";
219 
220 
221     private static class IncomingHandler extends Handler {
222         private final WeakReference<DynamicSystemClient> mWeakClient;
223 
IncomingHandler(DynamicSystemClient service)224         IncomingHandler(DynamicSystemClient service) {
225             super(Looper.getMainLooper());
226             mWeakClient = new WeakReference<>(service);
227         }
228 
229         @Override
handleMessage(Message msg)230         public void handleMessage(Message msg) {
231             DynamicSystemClient service = mWeakClient.get();
232 
233             if (service != null) {
234                 service.handleMessage(msg);
235             }
236         }
237     }
238 
239     private class DynSystemServiceConnection implements ServiceConnection {
onServiceConnected(ComponentName className, IBinder service)240         public void onServiceConnected(ComponentName className, IBinder service) {
241             Slog.v(TAG, "onServiceConnected: " + className);
242 
243             mService = new Messenger(service);
244 
245             try {
246                 Message msg = Message.obtain(null, MSG_REGISTER_LISTENER);
247                 msg.replyTo = mMessenger;
248 
249                 mService.send(msg);
250             } catch (RemoteException e) {
251                 Slog.e(TAG, "Unable to get status from installation service");
252                 notifyOnStatusChangedListener(STATUS_UNKNOWN, CAUSE_ERROR_IPC, 0, e);
253             }
254         }
255 
onServiceDisconnected(ComponentName className)256         public void onServiceDisconnected(ComponentName className) {
257             Slog.v(TAG, "onServiceDisconnected: " + className);
258             mService = null;
259         }
260     }
261 
262     private final Context mContext;
263     private final DynSystemServiceConnection mConnection;
264     private final Messenger mMessenger;
265 
266     private boolean mBound;
267     private Executor mExecutor;
268     private OnStatusChangedListener mListener;
269     private Messenger mService;
270 
271     /**
272      * Create a new {@code DynamicSystem} client.
273      *
274      * @param context a {@link Context} will be used to bind the installation service.
275      *
276      * @hide
277      */
278     @SystemApi
DynamicSystemClient(@onNull Context context)279     public DynamicSystemClient(@NonNull Context context) {
280         mContext = context;
281         mConnection = new DynSystemServiceConnection();
282         mMessenger = new Messenger(new IncomingHandler(this));
283     }
284 
285     /**
286      * This method register a listener for status change. The listener is called using
287      * the executor.
288      */
setOnStatusChangedListener( @onNull @allbackExecutor Executor executor, @NonNull OnStatusChangedListener listener)289     public void setOnStatusChangedListener(
290             @NonNull @CallbackExecutor Executor executor,
291             @NonNull OnStatusChangedListener listener) {
292         mListener = listener;
293         mExecutor = executor;
294     }
295 
296     /**
297      * This method register a listener for status change. The listener is called in main
298      * thread.
299      */
setOnStatusChangedListener( @onNull OnStatusChangedListener listener)300     public void setOnStatusChangedListener(
301             @NonNull OnStatusChangedListener listener) {
302         mListener = listener;
303         mExecutor = null;
304     }
305 
notifyOnStatusChangedListener( int status, int cause, long progress, Throwable detail)306     private void notifyOnStatusChangedListener(
307             int status, int cause, long progress, Throwable detail) {
308         if (mListener != null) {
309             if (mExecutor != null) {
310                 mExecutor.execute(
311                         () -> {
312                             mListener.onStatusChanged(status, cause, progress, detail);
313                         });
314             } else {
315                 mListener.onStatusChanged(status, cause, progress, detail);
316             }
317         }
318     }
319 
320     /**
321      * Bind to {@code DynamicSystem} installation service. Binding to the installation service
322      * allows it to send status updates to {@link #OnStatusChangedListener}. It is recommanded
323      * to bind before calling {@link #start} and get status updates.
324      * @hide
325      */
326     @RequiresPermission(android.Manifest.permission.INSTALL_DYNAMIC_SYSTEM)
327     @SystemApi
bind()328     public void bind() {
329         Intent intent = new Intent();
330         intent.setClassName("com.android.dynsystem",
331                 "com.android.dynsystem.DynamicSystemInstallationService");
332 
333         mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
334 
335         mBound = true;
336     }
337 
338     /**
339      * Unbind from {@code DynamicSystem} installation service. Unbinding from the installation
340      * service stops it from sending following status updates.
341      * @hide
342      */
343     @RequiresPermission(android.Manifest.permission.INSTALL_DYNAMIC_SYSTEM)
344     @SystemApi
unbind()345     public void unbind() {
346         if (!mBound) {
347             return;
348         }
349 
350         if (mService != null) {
351             try {
352                 Message msg = Message.obtain(null, MSG_UNREGISTER_LISTENER);
353                 msg.replyTo = mMessenger;
354                 mService.send(msg);
355             } catch (RemoteException e) {
356                 Slog.e(TAG, "Unable to unregister from installation service");
357             }
358         }
359 
360         // Detach our existing connection.
361         mContext.unbindService(mConnection);
362 
363         mBound = false;
364     }
365 
366     /**
367      * Start installing {@code DynamicSystem} from URL with default userdata size.
368      *
369      * Calling this function will first start an Activity to confirm device credential, using
370      * {@link KeyguardManager}. If it's confirmed, the installation service will be started.
371      *
372      * This function doesn't require prior calling {@link #bind}.
373      *
374      * @param systemUrl a network Uri, a file Uri or a content Uri pointing to a system image file.
375      * @param systemSize size of system image.
376      * @hide
377      */
378     @RequiresPermission(android.Manifest.permission.INSTALL_DYNAMIC_SYSTEM)
379     @SystemApi
start(@onNull Uri systemUrl, @BytesLong long systemSize)380     public void start(@NonNull Uri systemUrl, @BytesLong long systemSize) {
381         start(systemUrl, systemSize, 0 /* Use the default userdata size */);
382     }
383 
384     /**
385      * Start installing {@code DynamicSystem} from URL.
386      *
387      * Calling this function will first start an Activity to confirm device credential, using
388      * {@link KeyguardManager}. If it's confirmed, the installation service will be started.
389      *
390      * This function doesn't require prior calling {@link #bind}.
391      *
392      * @param systemUrl a network Uri, a file Uri or a content Uri pointing to a system image file.
393      * @param systemSize size of system image.
394      * @param userdataSize bytes reserved for userdata.
395      */
396     @RequiresPermission(android.Manifest.permission.INSTALL_DYNAMIC_SYSTEM)
start(@onNull Uri systemUrl, @BytesLong long systemSize, @BytesLong long userdataSize)397     public void start(@NonNull Uri systemUrl, @BytesLong long systemSize,
398             @BytesLong long userdataSize) {
399         Intent intent = new Intent();
400 
401         intent.setClassName("com.android.dynsystem",
402                 "com.android.dynsystem.VerificationActivity");
403 
404         intent.setData(systemUrl);
405         intent.setAction(ACTION_START_INSTALL);
406         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
407 
408         intent.putExtra(KEY_SYSTEM_SIZE, systemSize);
409         intent.putExtra(KEY_USERDATA_SIZE, userdataSize);
410 
411         mContext.startActivity(intent);
412     }
413 
handleMessage(Message msg)414     private void handleMessage(Message msg) {
415         switch (msg.what) {
416             case MSG_POST_STATUS:
417                 int status = msg.arg1;
418                 int cause = msg.arg2;
419                 // obj is non-null
420                 Bundle bundle = (Bundle) msg.obj;
421                 long progress = bundle.getLong(KEY_INSTALLED_SIZE);
422                 ParcelableException t = (ParcelableException) bundle.getSerializable(
423                         KEY_EXCEPTION_DETAIL);
424 
425                 Throwable detail = t == null ? null : t.getCause();
426 
427                 notifyOnStatusChangedListener(status, cause, progress, detail);
428                 break;
429             default:
430                 // do nothing
431 
432         }
433     }
434 }
435