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.dynsystem; 18 19 import static android.os.AsyncTask.Status.RUNNING; 20 import static android.os.image.DynamicSystemClient.ACTION_HIDE_NOTIFICATION; 21 import static android.os.image.DynamicSystemClient.ACTION_NOTIFY_IF_IN_USE; 22 import static android.os.image.DynamicSystemClient.ACTION_NOTIFY_KEYGUARD_DISMISSED; 23 import static android.os.image.DynamicSystemClient.ACTION_START_INSTALL; 24 import static android.os.image.DynamicSystemClient.CAUSE_ERROR_EXCEPTION; 25 import static android.os.image.DynamicSystemClient.CAUSE_ERROR_INVALID_URL; 26 import static android.os.image.DynamicSystemClient.CAUSE_ERROR_IO; 27 import static android.os.image.DynamicSystemClient.CAUSE_INSTALL_CANCELLED; 28 import static android.os.image.DynamicSystemClient.CAUSE_INSTALL_COMPLETED; 29 import static android.os.image.DynamicSystemClient.CAUSE_NOT_SPECIFIED; 30 import static android.os.image.DynamicSystemClient.KEY_ENABLE_WHEN_COMPLETED; 31 import static android.os.image.DynamicSystemClient.KEY_ONE_SHOT; 32 import static android.os.image.DynamicSystemClient.STATUS_IN_PROGRESS; 33 import static android.os.image.DynamicSystemClient.STATUS_IN_USE; 34 import static android.os.image.DynamicSystemClient.STATUS_NOT_STARTED; 35 import static android.os.image.DynamicSystemClient.STATUS_READY; 36 37 import static com.android.dynsystem.InstallationAsyncTask.RESULT_CANCELLED; 38 import static com.android.dynsystem.InstallationAsyncTask.RESULT_ERROR_EXCEPTION; 39 import static com.android.dynsystem.InstallationAsyncTask.RESULT_ERROR_IO; 40 import static com.android.dynsystem.InstallationAsyncTask.RESULT_ERROR_UNSUPPORTED_FORMAT; 41 import static com.android.dynsystem.InstallationAsyncTask.RESULT_ERROR_UNSUPPORTED_URL; 42 import static com.android.dynsystem.InstallationAsyncTask.RESULT_OK; 43 44 import android.app.Notification; 45 import android.app.NotificationChannel; 46 import android.app.NotificationManager; 47 import android.app.PendingIntent; 48 import android.app.Service; 49 import android.content.Context; 50 import android.content.Intent; 51 import android.net.http.HttpResponseCache; 52 import android.os.Bundle; 53 import android.os.Handler; 54 import android.os.IBinder; 55 import android.os.Message; 56 import android.os.Messenger; 57 import android.os.ParcelableException; 58 import android.os.PowerManager; 59 import android.os.RemoteException; 60 import android.os.image.DynamicSystemClient; 61 import android.os.image.DynamicSystemManager; 62 import android.text.TextUtils; 63 import android.util.Log; 64 import android.widget.Toast; 65 66 import java.io.File; 67 import java.io.IOException; 68 import java.lang.ref.WeakReference; 69 import java.util.ArrayList; 70 71 /** 72 * This class is the service in charge of DynamicSystem installation. 73 * It also posts status to notification bar and wait for user's 74 * cancel and confirm commnands. 75 */ 76 public class DynamicSystemInstallationService extends Service 77 implements InstallationAsyncTask.ProgressListener { 78 79 private static final String TAG = "DynamicSystemInstallationService"; 80 81 static final String KEY_DSU_SLOT = "KEY_DSU_SLOT"; 82 static final String DEFAULT_DSU_SLOT = "dsu"; 83 static final String KEY_PUBKEY = "KEY_PUBKEY"; 84 85 // Default userdata partition size is 2GiB. 86 private static final long DEFAULT_USERDATA_SIZE = 2L << 30; 87 88 /* 89 * Intent actions 90 */ 91 private static final String ACTION_CANCEL_INSTALL = 92 "com.android.dynsystem.ACTION_CANCEL_INSTALL"; 93 private static final String ACTION_DISCARD_INSTALL = 94 "com.android.dynsystem.ACTION_DISCARD_INSTALL"; 95 private static final String ACTION_REBOOT_TO_DYN_SYSTEM = 96 "com.android.dynsystem.ACTION_REBOOT_TO_DYN_SYSTEM"; 97 private static final String ACTION_REBOOT_TO_NORMAL = 98 "com.android.dynsystem.ACTION_REBOOT_TO_NORMAL"; 99 100 /* 101 * For notification 102 */ 103 private static final String NOTIFICATION_CHANNEL_ID = "com.android.dynsystem"; 104 private static final int NOTIFICATION_ID = 1; 105 logEventProgressUpdate( String partitionName, long installedBytes, long totalBytes, int partitionNumber, int totalPartitionNumber, int totalProgressPercentage)106 protected static void logEventProgressUpdate( 107 String partitionName, 108 long installedBytes, 109 long totalBytes, 110 int partitionNumber, 111 int totalPartitionNumber, 112 int totalProgressPercentage) { 113 EventLogTags.writeDsuProgressUpdate( 114 partitionName, 115 installedBytes, 116 totalBytes, 117 partitionNumber, 118 totalPartitionNumber, 119 totalProgressPercentage); 120 } 121 logEventComplete()122 protected static void logEventComplete() { 123 EventLogTags.writeDsuInstallComplete(); 124 } 125 logEventFailed(String cause)126 protected static void logEventFailed(String cause) { 127 EventLogTags.writeDsuInstallFailed(cause); 128 } 129 logEventInsufficientSpace()130 protected static void logEventInsufficientSpace() { 131 EventLogTags.writeDsuInstallInsufficientSpace(); 132 } 133 134 /* 135 * IPC 136 */ 137 /** Keeps track of all current registered clients. */ 138 ArrayList<Messenger> mClients = new ArrayList<>(); 139 140 /** Handler of incoming messages from clients. */ 141 final Messenger mMessenger = new Messenger(new IncomingHandler(this)); 142 143 static class IncomingHandler extends Handler { 144 private final WeakReference<DynamicSystemInstallationService> mWeakService; 145 IncomingHandler(DynamicSystemInstallationService service)146 IncomingHandler(DynamicSystemInstallationService service) { 147 mWeakService = new WeakReference<>(service); 148 } 149 150 @Override handleMessage(Message msg)151 public void handleMessage(Message msg) { 152 DynamicSystemInstallationService service = mWeakService.get(); 153 154 if (service != null) { 155 service.handleMessage(msg); 156 } 157 } 158 } 159 160 private DynamicSystemManager mDynSystem; 161 private NotificationManager mNM; 162 163 // This is for testing only now 164 private boolean mEnableWhenCompleted; 165 private boolean mOneShot = true; 166 private boolean mHideNotification; 167 168 private InstallationAsyncTask.Progress mInstallTaskProgress; 169 private InstallationAsyncTask mInstallTask; 170 171 172 @Override onCreate()173 public void onCreate() { 174 super.onCreate(); 175 176 prepareNotification(); 177 178 mDynSystem = (DynamicSystemManager) getSystemService(Context.DYNAMIC_SYSTEM_SERVICE); 179 180 // Install an HttpResponseCache in the application cache directory so we can cache 181 // gsi key revocation list. The http(s) protocol handler uses this cache transparently. 182 // The cache size is chosen heuristically. Since we don't have too much traffic right now, 183 // a moderate size of 1MiB should be enough. 184 try { 185 File httpCacheDir = new File(getCacheDir(), "httpCache"); 186 long httpCacheSize = 1 * 1024 * 1024; // 1 MiB 187 HttpResponseCache.install(httpCacheDir, httpCacheSize); 188 } catch (IOException e) { 189 Log.d(TAG, "HttpResponseCache.install() failed: " + e); 190 } 191 } 192 193 @Override onDestroy()194 public void onDestroy() { 195 HttpResponseCache cache = HttpResponseCache.getInstalled(); 196 if (cache != null) { 197 cache.flush(); 198 } 199 } 200 201 @Override onBind(Intent intent)202 public IBinder onBind(Intent intent) { 203 return mMessenger.getBinder(); 204 } 205 206 @Override onStartCommand(Intent intent, int flags, int startId)207 public int onStartCommand(Intent intent, int flags, int startId) { 208 String action = intent.getAction(); 209 210 Log.d(TAG, "onStartCommand(): action=" + action); 211 212 if (ACTION_START_INSTALL.equals(action)) { 213 executeInstallCommand(intent); 214 } else if (ACTION_CANCEL_INSTALL.equals(action)) { 215 executeCancelCommand(); 216 } else if (ACTION_DISCARD_INSTALL.equals(action)) { 217 executeDiscardCommand(); 218 } else if (ACTION_REBOOT_TO_DYN_SYSTEM.equals(action)) { 219 executeRebootToDynSystemCommand(); 220 } else if (ACTION_REBOOT_TO_NORMAL.equals(action)) { 221 executeRebootToNormalCommand(); 222 } else if (ACTION_NOTIFY_IF_IN_USE.equals(action)) { 223 executeNotifyIfInUseCommand(); 224 } else if (ACTION_HIDE_NOTIFICATION.equals(action)) { 225 executeHideNotificationCommand(); 226 } else if (ACTION_NOTIFY_KEYGUARD_DISMISSED.equals(action)) { 227 executeNotifyKeyguardDismissed(); 228 } 229 230 return Service.START_NOT_STICKY; 231 } 232 233 @Override onProgressUpdate(InstallationAsyncTask.Progress progress)234 public void onProgressUpdate(InstallationAsyncTask.Progress progress) { 235 logEventProgressUpdate( 236 progress.partitionName, 237 progress.installedBytes, 238 progress.totalBytes, 239 progress.partitionNumber, 240 progress.totalPartitionNumber, 241 progress.totalProgressPercentage); 242 243 mInstallTaskProgress = progress; 244 postStatus(STATUS_IN_PROGRESS, CAUSE_NOT_SPECIFIED, null); 245 } 246 247 @Override onResult(int result, Throwable detail)248 public void onResult(int result, Throwable detail) { 249 if (result == RESULT_OK) { 250 logEventComplete(); 251 postStatus(STATUS_READY, CAUSE_INSTALL_COMPLETED, null); 252 253 // For testing: enable DSU and restart the device when install completed 254 if (mEnableWhenCompleted) { 255 executeRebootToDynSystemCommand(); 256 } 257 return; 258 } 259 260 if (result == RESULT_CANCELLED) { 261 logEventFailed("Dynamic System installation task is canceled by the user."); 262 } else if (detail instanceof InstallationAsyncTask.InsufficientSpaceException) { 263 logEventInsufficientSpace(); 264 } else { 265 logEventFailed("error: " + detail); 266 } 267 268 boolean removeNotification = false; 269 switch (result) { 270 case RESULT_CANCELLED: 271 postStatus(STATUS_NOT_STARTED, CAUSE_INSTALL_CANCELLED, null); 272 removeNotification = true; 273 break; 274 275 case RESULT_ERROR_IO: 276 postStatus(STATUS_NOT_STARTED, CAUSE_ERROR_IO, detail); 277 break; 278 279 case RESULT_ERROR_UNSUPPORTED_URL: 280 case RESULT_ERROR_UNSUPPORTED_FORMAT: 281 postStatus(STATUS_NOT_STARTED, CAUSE_ERROR_INVALID_URL, detail); 282 break; 283 284 case RESULT_ERROR_EXCEPTION: 285 postStatus(STATUS_NOT_STARTED, CAUSE_ERROR_EXCEPTION, detail); 286 break; 287 } 288 289 // if it's not successful, reset the task and stop self. 290 resetTaskAndStop(removeNotification); 291 } 292 executeInstallCommand(Intent intent)293 private void executeInstallCommand(Intent intent) { 294 if (!verifyRequest(intent)) { 295 Log.e(TAG, "Verification failed. Did you use VerificationActivity?"); 296 logEventFailed("VerificationActivity"); 297 return; 298 } 299 300 if (mInstallTask != null) { 301 Log.e(TAG, "There is already an installation task running"); 302 logEventFailed("There is already an ongoing installation task."); 303 return; 304 } 305 306 if (isInDynamicSystem()) { 307 Log.e(TAG, "We are already running in DynamicSystem"); 308 logEventFailed( 309 "Cannot start a Dynamic System installation task within a Dynamic System."); 310 return; 311 } 312 313 String url = intent.getDataString(); 314 long systemSize = intent.getLongExtra(DynamicSystemClient.KEY_SYSTEM_SIZE, 0); 315 long userdataSize = intent.getLongExtra(DynamicSystemClient.KEY_USERDATA_SIZE, 0); 316 mEnableWhenCompleted = intent.getBooleanExtra(KEY_ENABLE_WHEN_COMPLETED, false); 317 mOneShot = intent.getBooleanExtra(KEY_ONE_SHOT, true); 318 String dsuSlot = intent.getStringExtra(KEY_DSU_SLOT); 319 String publicKey = intent.getStringExtra(KEY_PUBKEY); 320 321 if (userdataSize == 0) { 322 userdataSize = DEFAULT_USERDATA_SIZE; 323 } 324 325 if (TextUtils.isEmpty(dsuSlot)) { 326 dsuSlot = DEFAULT_DSU_SLOT; 327 } 328 // TODO: better constructor or builder 329 mInstallTask = 330 new InstallationAsyncTask( 331 url, dsuSlot, publicKey, systemSize, userdataSize, this, mDynSystem, this); 332 333 mInstallTask.execute(); 334 335 // start fore ground 336 startForeground(NOTIFICATION_ID, 337 buildNotification(STATUS_IN_PROGRESS, CAUSE_NOT_SPECIFIED)); 338 } 339 executeCancelCommand()340 private void executeCancelCommand() { 341 if (mInstallTask == null || mInstallTask.getStatus() != RUNNING) { 342 Log.e(TAG, "Cancel command triggered, but there is no task running"); 343 return; 344 } 345 346 if (mInstallTask.cancel(false)) { 347 // onResult() would call resetTaskAndStop() upon task completion. 348 Log.d(TAG, "Cancel request filed successfully"); 349 // Dismiss the notification as soon as possible as DynamicSystemManager.remove() may 350 // block. 351 stopForeground(STOP_FOREGROUND_REMOVE); 352 } else { 353 Log.e(TAG, "Trying to cancel installation while it's already completed."); 354 } 355 } 356 executeDiscardCommand()357 private void executeDiscardCommand() { 358 if (isInDynamicSystem()) { 359 Log.e(TAG, "We are now running in AOT, please reboot to normal system first"); 360 return; 361 } 362 363 if (!isDynamicSystemInstalled() && (getStatus() != STATUS_READY)) { 364 Log.e(TAG, "Trying to discard AOT while there is no complete installation"); 365 // Stop foreground state and dismiss stale notification. 366 resetTaskAndStop(true); 367 return; 368 } 369 370 Toast.makeText(this, 371 getString(R.string.toast_dynsystem_discarded), 372 Toast.LENGTH_LONG).show(); 373 374 postStatus(STATUS_NOT_STARTED, CAUSE_INSTALL_CANCELLED, null); 375 resetTaskAndStop(true); 376 377 mDynSystem.remove(); 378 } 379 executeRebootToDynSystemCommand()380 private void executeRebootToDynSystemCommand() { 381 boolean enabled = false; 382 383 if (mInstallTask != null && mInstallTask.isCompleted()) { 384 enabled = mInstallTask.commit(mOneShot); 385 } else if (isDynamicSystemInstalled()) { 386 enabled = mDynSystem.setEnable(true, mOneShot); 387 } else { 388 Log.e(TAG, "Trying to reboot to AOT while there is no complete installation"); 389 return; 390 } 391 392 if (enabled) { 393 PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); 394 395 if (powerManager != null) { 396 powerManager.reboot("dynsystem"); 397 } 398 return; 399 } 400 401 Log.e(TAG, "Failed to enable DynamicSystem because of native runtime error."); 402 403 Toast.makeText(this, 404 getString(R.string.toast_failed_to_reboot_to_dynsystem), 405 Toast.LENGTH_LONG).show(); 406 407 postStatus(STATUS_NOT_STARTED, CAUSE_ERROR_EXCEPTION, null); 408 resetTaskAndStop(); 409 mDynSystem.remove(); 410 } 411 isDsuSlotLocked()412 private boolean isDsuSlotLocked() { 413 // Slot names ending with ".lock" are a customized installation. 414 // We expect the client app to provide custom UI to enter/exit DSU mode. 415 // We will ignore the ACTION_REBOOT_TO_NORMAL command and will not show 416 // notifications in this case. 417 return mDynSystem.getActiveDsuSlot().endsWith(".lock"); 418 } 419 executeRebootToNormalCommand()420 private void executeRebootToNormalCommand() { 421 if (!isInDynamicSystem()) { 422 Log.e(TAG, "It's already running in normal system."); 423 return; 424 } 425 if (isDsuSlotLocked()) { 426 Log.e(TAG, "Ignore the reboot intent for a locked DSU slot"); 427 return; 428 } 429 if (!mDynSystem.setEnable(/* enable = */ false, /* oneShot = */ false)) { 430 Log.e(TAG, "Failed to disable DynamicSystem."); 431 432 // Dismiss status bar and show a toast. 433 closeSystemDialogs(); 434 Toast.makeText(this, 435 getString(R.string.toast_failed_to_disable_dynsystem), 436 Toast.LENGTH_LONG).show(); 437 return; 438 } 439 440 PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); 441 442 if (powerManager != null) { 443 powerManager.reboot(null); 444 } 445 } 446 executeNotifyIfInUseCommand()447 private void executeNotifyIfInUseCommand() { 448 switch (getStatus()) { 449 case STATUS_IN_USE: 450 if (!mHideNotification && !isDsuSlotLocked()) { 451 startForeground(NOTIFICATION_ID, 452 buildNotification(STATUS_IN_USE, CAUSE_NOT_SPECIFIED)); 453 } 454 break; 455 case STATUS_READY: 456 if (!mHideNotification && !isDsuSlotLocked()) { 457 startForeground(NOTIFICATION_ID, 458 buildNotification(STATUS_READY, CAUSE_NOT_SPECIFIED)); 459 } 460 break; 461 case STATUS_IN_PROGRESS: 462 break; 463 case STATUS_NOT_STARTED: 464 default: 465 stopSelf(); 466 } 467 } 468 executeHideNotificationCommand()469 private void executeHideNotificationCommand() { 470 mHideNotification = true; 471 switch (getStatus()) { 472 case STATUS_IN_USE: 473 case STATUS_READY: 474 stopForeground(STOP_FOREGROUND_REMOVE); 475 break; 476 } 477 } 478 executeNotifyKeyguardDismissed()479 private void executeNotifyKeyguardDismissed() { 480 postStatus(STATUS_NOT_STARTED, CAUSE_INSTALL_CANCELLED, null); 481 } 482 resetTaskAndStop()483 private void resetTaskAndStop() { 484 resetTaskAndStop(/* removeNotification= */ false); 485 } 486 resetTaskAndStop(boolean removeNotification)487 private void resetTaskAndStop(boolean removeNotification) { 488 mInstallTask = null; 489 stopForeground(removeNotification ? STOP_FOREGROUND_REMOVE : STOP_FOREGROUND_DETACH); 490 stopSelf(); 491 } 492 prepareNotification()493 private void prepareNotification() { 494 NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, 495 getString(R.string.notification_channel_name), 496 NotificationManager.IMPORTANCE_LOW); 497 498 mNM = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); 499 500 if (mNM != null) { 501 mNM.createNotificationChannel(chan); 502 } 503 } 504 createPendingIntent(String action)505 private PendingIntent createPendingIntent(String action) { 506 Intent intent = new Intent(this, DynamicSystemInstallationService.class); 507 intent.setAction(action); 508 return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_IMMUTABLE); 509 } 510 buildNotification(int status, int cause)511 private Notification buildNotification(int status, int cause) { 512 return buildNotification(status, cause, null); 513 } 514 buildNotification(int status, int cause, Throwable detail)515 private Notification buildNotification(int status, int cause, Throwable detail) { 516 Notification.Builder builder = new Notification.Builder(this, NOTIFICATION_CHANNEL_ID) 517 .setSmallIcon(R.drawable.ic_system_update_googblue_24dp) 518 .setProgress(0, 0, false); 519 520 switch (status) { 521 case STATUS_IN_PROGRESS: 522 String msgInProgress = getString(R.string.notification_install_inprogress); 523 524 if (mInstallTaskProgress == null) { 525 builder.setContentText(msgInProgress); 526 } else { 527 if (mInstallTaskProgress.totalPartitionNumber > 0) { 528 builder.setContentText( 529 String.format( 530 "%s: %s partition [%d/%d]", 531 msgInProgress, 532 mInstallTaskProgress.partitionName, 533 mInstallTaskProgress.partitionNumber, 534 mInstallTaskProgress.totalPartitionNumber)); 535 536 // totalProgressPercentage is defined iff totalPartitionNumber is defined 537 builder.setProgress( 538 100, 539 mInstallTaskProgress.totalProgressPercentage, 540 /* indeterminate = */ false); 541 } else { 542 builder.setContentText( 543 String.format( 544 "%s: %s partition", 545 msgInProgress, mInstallTaskProgress.partitionName)); 546 547 int max = 1024; 548 int progress = 0; 549 550 int currentMax = max >> mInstallTaskProgress.partitionNumber; 551 progress = max - currentMax * 2; 552 553 long currentProgress = 554 (mInstallTaskProgress.installedBytes >> 20) 555 * currentMax 556 / Math.max(mInstallTaskProgress.totalBytes >> 20, 1); 557 558 progress += (int) currentProgress; 559 560 builder.setProgress(max, progress, false); 561 } 562 } 563 builder.addAction(new Notification.Action.Builder( 564 null, getString(R.string.notification_action_cancel), 565 createPendingIntent(ACTION_CANCEL_INSTALL)).build()); 566 567 break; 568 569 case STATUS_READY: 570 String msgCompleted = getString(R.string.notification_install_completed); 571 builder.setContentText(msgCompleted) 572 .setStyle(new Notification.BigTextStyle().bigText(msgCompleted)); 573 574 builder.addAction(new Notification.Action.Builder( 575 null, getString(R.string.notification_action_discard), 576 createPendingIntent(ACTION_DISCARD_INSTALL)).build()); 577 578 builder.addAction(new Notification.Action.Builder( 579 null, getString(R.string.notification_action_reboot_to_dynsystem), 580 createPendingIntent(ACTION_REBOOT_TO_DYN_SYSTEM)).build()); 581 582 break; 583 584 case STATUS_IN_USE: 585 String msgInUse = getString(R.string.notification_dynsystem_in_use); 586 builder.setContentText(msgInUse) 587 .setStyle(new Notification.BigTextStyle().bigText(msgInUse)); 588 589 builder.addAction(new Notification.Action.Builder( 590 null, getString(R.string.notification_action_reboot_to_origin), 591 createPendingIntent(ACTION_REBOOT_TO_NORMAL)).build()); 592 593 break; 594 595 case STATUS_NOT_STARTED: 596 if (cause != CAUSE_NOT_SPECIFIED && cause != CAUSE_INSTALL_CANCELLED) { 597 if (detail instanceof InstallationAsyncTask.ImageValidationException) { 598 builder.setContentText( 599 getString(R.string.notification_image_validation_failed)); 600 } else { 601 builder.setContentText(getString(R.string.notification_install_failed)); 602 } 603 } else { 604 // no need to notify the user if the task is not started, or cancelled. 605 } 606 break; 607 608 default: 609 throw new IllegalStateException("status is invalid"); 610 } 611 612 return builder.build(); 613 } 614 verifyRequest(Intent intent)615 private boolean verifyRequest(Intent intent) { 616 String url = intent.getDataString(); 617 618 return VerificationActivity.isVerified(url); 619 } 620 postStatus(int status, int cause, Throwable detail)621 private void postStatus(int status, int cause, Throwable detail) { 622 String statusString; 623 String causeString; 624 boolean notifyOnNotificationBar = true; 625 626 switch (status) { 627 case STATUS_NOT_STARTED: 628 statusString = "NOT_STARTED"; 629 break; 630 case STATUS_IN_PROGRESS: 631 statusString = "IN_PROGRESS"; 632 break; 633 case STATUS_READY: 634 statusString = "READY"; 635 break; 636 case STATUS_IN_USE: 637 statusString = "IN_USE"; 638 break; 639 default: 640 statusString = "UNKNOWN"; 641 break; 642 } 643 644 switch (cause) { 645 case CAUSE_INSTALL_COMPLETED: 646 causeString = "INSTALL_COMPLETED"; 647 break; 648 case CAUSE_INSTALL_CANCELLED: 649 causeString = "INSTALL_CANCELLED"; 650 notifyOnNotificationBar = false; 651 break; 652 case CAUSE_ERROR_IO: 653 causeString = "ERROR_IO"; 654 break; 655 case CAUSE_ERROR_INVALID_URL: 656 causeString = "ERROR_INVALID_URL"; 657 break; 658 case CAUSE_ERROR_EXCEPTION: 659 causeString = "ERROR_EXCEPTION"; 660 break; 661 default: 662 causeString = "CAUSE_NOT_SPECIFIED"; 663 break; 664 } 665 666 StringBuilder msg = new StringBuilder(); 667 msg.append("status: " + statusString + ", cause: " + causeString); 668 if (status == STATUS_IN_PROGRESS && mInstallTaskProgress != null) { 669 msg.append( 670 String.format( 671 ", partition name: %s, progress: %d/%d, total_progress: %d%%", 672 mInstallTaskProgress.partitionName, 673 mInstallTaskProgress.installedBytes, 674 mInstallTaskProgress.totalBytes, 675 mInstallTaskProgress.totalProgressPercentage)); 676 } 677 if (detail != null) { 678 msg.append(", detail: " + detail); 679 } 680 Log.d(TAG, msg.toString()); 681 682 if (notifyOnNotificationBar) { 683 mNM.notify(NOTIFICATION_ID, buildNotification(status, cause, detail)); 684 } 685 686 for (int i = mClients.size() - 1; i >= 0; i--) { 687 try { 688 notifyOneClient(mClients.get(i), status, cause, detail); 689 } catch (RemoteException e) { 690 mClients.remove(i); 691 } 692 } 693 } 694 notifyOneClient(Messenger client, int status, int cause, Throwable detail)695 private void notifyOneClient(Messenger client, int status, int cause, Throwable detail) 696 throws RemoteException { 697 Bundle bundle = new Bundle(); 698 699 // TODO: send more info to the clients 700 if (mInstallTaskProgress != null) { 701 bundle.putLong( 702 DynamicSystemClient.KEY_INSTALLED_SIZE, mInstallTaskProgress.installedBytes); 703 } 704 705 if (detail != null) { 706 bundle.putSerializable(DynamicSystemClient.KEY_EXCEPTION_DETAIL, 707 new ParcelableException(detail)); 708 } 709 710 client.send(Message.obtain(null, 711 DynamicSystemClient.MSG_POST_STATUS, status, cause, bundle)); 712 } 713 getStatus()714 private int getStatus() { 715 if (isInDynamicSystem()) { 716 return STATUS_IN_USE; 717 } else if (isDynamicSystemInstalled()) { 718 return STATUS_READY; 719 } else if (mInstallTask == null) { 720 return STATUS_NOT_STARTED; 721 } 722 723 switch (mInstallTask.getStatus()) { 724 case PENDING: 725 return STATUS_NOT_STARTED; 726 727 case RUNNING: 728 return STATUS_IN_PROGRESS; 729 730 case FINISHED: 731 if (mInstallTask.isCompleted()) { 732 return STATUS_READY; 733 } else { 734 throw new IllegalStateException("A failed InstallationTask is not reset"); 735 } 736 737 default: 738 return STATUS_NOT_STARTED; 739 } 740 } 741 isInDynamicSystem()742 private boolean isInDynamicSystem() { 743 return mDynSystem.isInUse(); 744 } 745 isDynamicSystemInstalled()746 private boolean isDynamicSystemInstalled() { 747 return mDynSystem.isInstalled(); 748 } 749 handleMessage(Message msg)750 void handleMessage(Message msg) { 751 switch (msg.what) { 752 case DynamicSystemClient.MSG_REGISTER_LISTENER: 753 try { 754 Messenger client = msg.replyTo; 755 756 int status = getStatus(); 757 758 // tell just registered client my status, but do not specify cause 759 notifyOneClient(client, status, CAUSE_NOT_SPECIFIED, null); 760 761 mClients.add(client); 762 } catch (RemoteException e) { 763 // do nothing if we cannot send update to the client 764 e.printStackTrace(); 765 } 766 767 break; 768 case DynamicSystemClient.MSG_UNREGISTER_LISTENER: 769 mClients.remove(msg.replyTo); 770 break; 771 default: 772 // do nothing 773 } 774 } 775 } 776