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