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