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