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