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