blob: 63ac8c73decf43946a4815cb70afa216ab81850a [file] [log] [blame]
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +08001/*
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
17package com.android.dynandroid;
18
19import static android.content.DynamicAndroidClient.ACTION_NOTIFY_IF_IN_USE;
20import static android.content.DynamicAndroidClient.ACTION_START_INSTALL;
21import static android.content.DynamicAndroidClient.CAUSE_ERROR_EXCEPTION;
22import static android.content.DynamicAndroidClient.CAUSE_ERROR_INVALID_URL;
23import static android.content.DynamicAndroidClient.CAUSE_ERROR_IO;
24import static android.content.DynamicAndroidClient.CAUSE_INSTALL_CANCELLED;
25import static android.content.DynamicAndroidClient.CAUSE_INSTALL_COMPLETED;
26import static android.content.DynamicAndroidClient.CAUSE_NOT_SPECIFIED;
27import static android.content.DynamicAndroidClient.STATUS_IN_PROGRESS;
28import static android.content.DynamicAndroidClient.STATUS_IN_USE;
29import static android.content.DynamicAndroidClient.STATUS_NOT_STARTED;
30import static android.content.DynamicAndroidClient.STATUS_READY;
31import static android.os.AsyncTask.Status.FINISHED;
32import static android.os.AsyncTask.Status.PENDING;
33import static android.os.AsyncTask.Status.RUNNING;
34
35import static com.android.dynandroid.InstallationAsyncTask.RESULT_ERROR_EXCEPTION;
36import static com.android.dynandroid.InstallationAsyncTask.RESULT_ERROR_INVALID_URL;
37import static com.android.dynandroid.InstallationAsyncTask.RESULT_ERROR_IO;
38import static com.android.dynandroid.InstallationAsyncTask.RESULT_OK;
39
40import android.app.Notification;
41import android.app.NotificationChannel;
42import android.app.NotificationManager;
43import android.app.PendingIntent;
44import android.app.Service;
45import android.content.Context;
46import android.content.DynamicAndroidClient;
47import android.content.Intent;
48import android.os.Bundle;
49import android.os.DynamicAndroidManager;
50import android.os.Handler;
51import android.os.IBinder;
52import android.os.Message;
53import android.os.Messenger;
54import android.os.PowerManager;
55import android.os.RemoteException;
56import android.util.Log;
Po-Chien Hsueh34de5af2019-03-05 16:00:45 +080057import android.widget.Toast;
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +080058
59import java.lang.ref.WeakReference;
60import java.util.ArrayList;
61
62/**
63 * This class is the service in charge of DynamicAndroid installation.
64 * It also posts status to notification bar and wait for user's
65 * cancel and confirm commnands.
66 */
67public class DynamicAndroidInstallationService extends Service
68 implements InstallationAsyncTask.InstallStatusListener {
69
70 private static final String TAG = "DynAndroidInstallationService";
71
72 /*
73 * Intent actions
74 */
75 private static final String ACTION_CANCEL_INSTALL =
76 "com.android.dynandroid.ACTION_CANCEL_INSTALL";
Po-Chien Hsueh34de5af2019-03-05 16:00:45 +080077 private static final String ACTION_DISCARD_INSTALL =
78 "com.android.dynandroid.ACTION_DISCARD_INSTALL";
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +080079 private static final String ACTION_REBOOT_TO_DYN_ANDROID =
80 "com.android.dynandroid.ACTION_REBOOT_TO_DYN_ANDROID";
81 private static final String ACTION_REBOOT_TO_NORMAL =
82 "com.android.dynandroid.ACTION_REBOOT_TO_NORMAL";
83
84 /*
85 * For notification
86 */
87 private static final String NOTIFICATION_CHANNEL_ID = "com.android.dynandroid";
88 private static final int NOTIFICATION_ID = 1;
89
90 /*
91 * IPC
92 */
93 /** Keeps track of all current registered clients. */
94 ArrayList<Messenger> mClients = new ArrayList<>();
95
96 /** Handler of incoming messages from clients. */
97 final Messenger mMessenger = new Messenger(new IncomingHandler(this));
98
99 static class IncomingHandler extends Handler {
100 private final WeakReference<DynamicAndroidInstallationService> mWeakService;
101
102 IncomingHandler(DynamicAndroidInstallationService service) {
103 mWeakService = new WeakReference<>(service);
104 }
105
106 @Override
107 public void handleMessage(Message msg) {
108 DynamicAndroidInstallationService service = mWeakService.get();
109
110 if (service != null) {
111 service.handleMessage(msg);
112 }
113 }
114 }
115
116 private DynamicAndroidManager mDynAndroid;
117 private NotificationManager mNM;
118
119 private long mSystemSize;
Po-Chien Hsueh9c1500f2019-03-04 14:47:46 +0800120 private long mUserdataSize;
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800121 private long mInstalledSize;
122 private boolean mJustCancelledByUser;
123
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800124 private InstallationAsyncTask mInstallTask;
125
126
127 @Override
128 public void onCreate() {
129 super.onCreate();
130
131 prepareNotification();
132
133 mDynAndroid = (DynamicAndroidManager) getSystemService(Context.DYNAMIC_ANDROID_SERVICE);
134 }
135
136 @Override
137 public void onDestroy() {
138 // Cancel the persistent notification.
139 mNM.cancel(NOTIFICATION_ID);
140 }
141
142 @Override
143 public IBinder onBind(Intent intent) {
144 return mMessenger.getBinder();
145 }
146
147 @Override
148 public int onStartCommand(Intent intent, int flags, int startId) {
149 String action = intent.getAction();
150
151 Log.d(TAG, "onStartCommand(): action=" + action);
152
153 if (ACTION_START_INSTALL.equals(action)) {
154 executeInstallCommand(intent);
155 } else if (ACTION_CANCEL_INSTALL.equals(action)) {
156 executeCancelCommand();
Po-Chien Hsueh34de5af2019-03-05 16:00:45 +0800157 } else if (ACTION_DISCARD_INSTALL.equals(action)) {
158 executeDiscardCommand();
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800159 } else if (ACTION_REBOOT_TO_DYN_ANDROID.equals(action)) {
160 executeRebootToDynAndroidCommand();
161 } else if (ACTION_REBOOT_TO_NORMAL.equals(action)) {
162 executeRebootToNormalCommand();
163 } else if (ACTION_NOTIFY_IF_IN_USE.equals(action)) {
164 executeNotifyIfInUseCommand();
165 }
166
167 return Service.START_NOT_STICKY;
168 }
169
170 @Override
171 public void onProgressUpdate(long installedSize) {
172 mInstalledSize = installedSize;
173 postStatus(STATUS_IN_PROGRESS, CAUSE_NOT_SPECIFIED);
174 }
175
176 @Override
177 public void onResult(int result) {
178 if (result == RESULT_OK) {
179 postStatus(STATUS_READY, CAUSE_INSTALL_COMPLETED);
180 return;
181 }
182
183 // if it's not successful, reset the task and stop self.
184 resetTaskAndStop();
185
186 switch (result) {
187 case RESULT_ERROR_IO:
188 postStatus(STATUS_NOT_STARTED, CAUSE_ERROR_IO);
189 break;
190
191 case RESULT_ERROR_INVALID_URL:
192 postStatus(STATUS_NOT_STARTED, CAUSE_ERROR_INVALID_URL);
193 break;
194
195 case RESULT_ERROR_EXCEPTION:
196 postStatus(STATUS_NOT_STARTED, CAUSE_ERROR_EXCEPTION);
197 break;
198 }
199 }
200
201 @Override
202 public void onCancelled() {
203 resetTaskAndStop();
204 postStatus(STATUS_NOT_STARTED, CAUSE_INSTALL_CANCELLED);
205 }
206
207 private void executeInstallCommand(Intent intent) {
208 if (!verifyRequest(intent)) {
209 Log.e(TAG, "Verification failed. Did you use VerificationActivity?");
210 return;
211 }
212
213 if (mInstallTask != null) {
Po-Chien Hsueh34de5af2019-03-05 16:00:45 +0800214 Log.e(TAG, "There is already an installation task running");
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800215 return;
216 }
217
218 if (isInDynamicAndroid()) {
219 Log.e(TAG, "We are already running in DynamicAndroid");
220 return;
221 }
222
223 String url = intent.getStringExtra(DynamicAndroidClient.KEY_SYSTEM_URL);
224 mSystemSize = intent.getLongExtra(DynamicAndroidClient.KEY_SYSTEM_SIZE, 0);
Po-Chien Hsueh9c1500f2019-03-04 14:47:46 +0800225 mUserdataSize = intent.getLongExtra(DynamicAndroidClient.KEY_USERDATA_SIZE, 0);
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800226
Po-Chien Hsueh9c1500f2019-03-04 14:47:46 +0800227 mInstallTask = new InstallationAsyncTask(
228 url, mSystemSize, mUserdataSize, mDynAndroid, this);
229
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800230 mInstallTask.execute();
231
232 // start fore ground
233 startForeground(NOTIFICATION_ID,
234 buildNotification(STATUS_IN_PROGRESS, CAUSE_NOT_SPECIFIED));
235 }
236
237 private void executeCancelCommand() {
Po-Chien Hsueh34de5af2019-03-05 16:00:45 +0800238 if (mInstallTask == null || mInstallTask.getStatus() != RUNNING) {
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800239 Log.e(TAG, "Cancel command triggered, but there is no task running");
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800240 return;
241 }
242
243 mJustCancelledByUser = true;
244
245 if (mInstallTask.cancel(false)) {
246 // Will cleanup and post status in onCancelled()
247 Log.d(TAG, "Cancel request filed successfully");
248 } else {
Po-Chien Hsueh34de5af2019-03-05 16:00:45 +0800249 Log.e(TAG, "Trying to cancel installation while it's already completed.");
250 }
251 }
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800252
Po-Chien Hsueh34de5af2019-03-05 16:00:45 +0800253 private void executeDiscardCommand() {
254 if (isInDynamicAndroid()) {
255 Log.e(TAG, "We are now running in AOT, please reboot to normal system first");
256 return;
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800257 }
258
Po-Chien Hsueh34de5af2019-03-05 16:00:45 +0800259 if (getStatus() != STATUS_READY) {
260 Log.e(TAG, "Trying to discard AOT while there is no complete installation");
261 return;
262 }
263
264 Toast.makeText(this,
265 getString(R.string.toast_dynandroid_discarded),
266 Toast.LENGTH_LONG).show();
267
268 resetTaskAndStop();
269 postStatus(STATUS_NOT_STARTED, CAUSE_INSTALL_CANCELLED);
270
271 mDynAndroid.remove();
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800272 }
273
274 private void executeRebootToDynAndroidCommand() {
275 if (mInstallTask == null || mInstallTask.getStatus() != FINISHED) {
Po-Chien Hsueh34de5af2019-03-05 16:00:45 +0800276 Log.e(TAG, "Trying to reboot to AOT while there is no complete installation");
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800277 return;
278 }
279
280 if (!mInstallTask.commit()) {
281 // TODO: b/123673280 better UI response
282 Log.e(TAG, "Failed to commit installation because of native runtime error.");
283 mNM.cancel(NOTIFICATION_ID);
284
285 return;
286 }
287
288 PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
289
290 if (powerManager != null) {
291 powerManager.reboot("dynandroid");
292 }
293 }
294
295 private void executeRebootToNormalCommand() {
Po-Chien Hsueh34de5af2019-03-05 16:00:45 +0800296 if (!isInDynamicAndroid()) {
297 Log.e(TAG, "It's already running in normal system.");
298 return;
299 }
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800300
Po-Chien Hsueh34de5af2019-03-05 16:00:45 +0800301 // Per current design, we don't have disable() API. AOT is disabled on next reboot.
302 // TODO: Use better status query when b/125079548 is done.
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800303 PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
304
305 if (powerManager != null) {
306 powerManager.reboot(null);
307 }
308 }
309
310 private void executeNotifyIfInUseCommand() {
Po-Chien Hsueh34de5af2019-03-05 16:00:45 +0800311 int status = getStatus();
312
313 if (status == STATUS_IN_USE) {
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800314 startForeground(NOTIFICATION_ID,
315 buildNotification(STATUS_IN_USE, CAUSE_NOT_SPECIFIED));
Po-Chien Hsueh34de5af2019-03-05 16:00:45 +0800316 } else if (status == STATUS_READY) {
317 startForeground(NOTIFICATION_ID,
318 buildNotification(STATUS_READY, CAUSE_NOT_SPECIFIED));
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800319 }
320 }
321
322 private void resetTaskAndStop() {
323 mInstallTask = null;
324
325 stopForeground(true);
326
327 // stop self, but this service is not destroyed yet if it's still bound
328 stopSelf();
329 }
330
331 private void prepareNotification() {
332 NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID,
333 getString(R.string.notification_channel_name),
334 NotificationManager.IMPORTANCE_LOW);
335
336 mNM = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
337
338 if (mNM != null) {
339 mNM.createNotificationChannel(chan);
340 }
Po-Chien Hsueh34de5af2019-03-05 16:00:45 +0800341 }
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800342
Po-Chien Hsueh34de5af2019-03-05 16:00:45 +0800343 private PendingIntent createPendingIntent(String action) {
344 Intent intent = new Intent(this, DynamicAndroidInstallationService.class);
345 intent.setAction(action);
346 return PendingIntent.getService(this, 0, intent, 0);
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800347 }
348
349 private Notification buildNotification(int status, int cause) {
350 Notification.Builder builder = new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
351 .setSmallIcon(R.drawable.ic_system_update_googblue_24dp)
352 .setProgress(0, 0, false);
353
354 switch (status) {
355 case STATUS_IN_PROGRESS:
356 builder.setContentText(getString(R.string.notification_install_inprogress));
357
Po-Chien Hsueh9c1500f2019-03-04 14:47:46 +0800358 int max = (int) Math.max((mSystemSize + mUserdataSize) >> 20, 1);
359 int progress = (int) (mInstalledSize >> 20);
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800360
361 builder.setProgress(max, progress, false);
362
363 builder.addAction(new Notification.Action.Builder(
364 null, getString(R.string.notification_action_cancel),
Po-Chien Hsueh34de5af2019-03-05 16:00:45 +0800365 createPendingIntent(ACTION_CANCEL_INSTALL)).build());
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800366
367 break;
368
369 case STATUS_READY:
370 builder.setContentText(getString(R.string.notification_install_completed));
371
372 builder.addAction(new Notification.Action.Builder(
373 null, getString(R.string.notification_action_reboot_to_dynandroid),
Po-Chien Hsueh34de5af2019-03-05 16:00:45 +0800374 createPendingIntent(ACTION_REBOOT_TO_DYN_ANDROID)).build());
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800375
376 builder.addAction(new Notification.Action.Builder(
Po-Chien Hsueh34de5af2019-03-05 16:00:45 +0800377 null, getString(R.string.notification_action_discard),
378 createPendingIntent(ACTION_DISCARD_INSTALL)).build());
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800379
380 break;
381
382 case STATUS_IN_USE:
383 builder.setContentText(getString(R.string.notification_dynandroid_in_use));
384
385 builder.addAction(new Notification.Action.Builder(
386 null, getString(R.string.notification_action_uninstall),
Po-Chien Hsueh34de5af2019-03-05 16:00:45 +0800387 createPendingIntent(ACTION_REBOOT_TO_NORMAL)).build());
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800388
389 break;
390
391 case STATUS_NOT_STARTED:
392 if (cause != CAUSE_NOT_SPECIFIED && cause != CAUSE_INSTALL_CANCELLED) {
393 builder.setContentText(getString(R.string.notification_install_failed));
394 } else {
395 // no need to notify the user if the task is not started, or cancelled.
396 }
397 break;
398
399 default:
400 throw new IllegalStateException("status is invalid");
401 }
402
403 return builder.build();
404 }
405
406 private boolean verifyRequest(Intent intent) {
407 String url = intent.getStringExtra(DynamicAndroidClient.KEY_SYSTEM_URL);
408
409 return VerificationActivity.isVerified(url);
410 }
411
412 private void postStatus(int status, int cause) {
413 Log.d(TAG, "postStatus(): statusCode=" + status + ", causeCode=" + cause);
414
415 boolean notifyOnNotificationBar = true;
416
417 if (status == STATUS_NOT_STARTED
418 && cause == CAUSE_INSTALL_CANCELLED
419 && mJustCancelledByUser) {
420 // if task is cancelled by user, do not notify them
421 notifyOnNotificationBar = false;
422 mJustCancelledByUser = false;
423 }
424
425 if (notifyOnNotificationBar) {
426 mNM.notify(NOTIFICATION_ID, buildNotification(status, cause));
427 }
428
429 for (int i = mClients.size() - 1; i >= 0; i--) {
430 try {
431 notifyOneClient(mClients.get(i), status, cause);
432 } catch (RemoteException e) {
433 mClients.remove(i);
434 }
435 }
436 }
437
438 private void notifyOneClient(Messenger client, int status, int cause) throws RemoteException {
439 Bundle bundle = new Bundle();
440
441 bundle.putLong(DynamicAndroidClient.KEY_INSTALLED_SIZE, mInstalledSize);
442
443 client.send(Message.obtain(null,
444 DynamicAndroidClient.MSG_POST_STATUS, status, cause, bundle));
445 }
446
447 private int getStatus() {
448 if (isInDynamicAndroid()) {
449 return STATUS_IN_USE;
Po-Chien Hsueh34de5af2019-03-05 16:00:45 +0800450 } else if (isDynamicAndroidInstalled()) {
451 return STATUS_READY;
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800452 } else if (mInstallTask == null) {
453 return STATUS_NOT_STARTED;
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800454 }
455
456 switch (mInstallTask.getStatus()) {
457 case PENDING:
458 return STATUS_NOT_STARTED;
459
460 case RUNNING:
461 return STATUS_IN_PROGRESS;
462
463 case FINISHED:
464 int result = mInstallTask.getResult();
465
466 if (result == RESULT_OK) {
467 return STATUS_READY;
468 } else {
469 throw new IllegalStateException("A failed InstallationTask is not reset");
470 }
471
472 default:
473 return STATUS_NOT_STARTED;
474 }
475 }
476
477 private boolean isInDynamicAndroid() {
478 return mDynAndroid.isInUse();
479 }
480
Po-Chien Hsueh34de5af2019-03-05 16:00:45 +0800481 private boolean isDynamicAndroidInstalled() {
482 return mDynAndroid.isInstalled();
483 }
484
Po-Chien Hsueh64aa7822019-01-12 00:40:02 +0800485 void handleMessage(Message msg) {
486 switch (msg.what) {
487 case DynamicAndroidClient.MSG_REGISTER_LISTENER:
488 try {
489 Messenger client = msg.replyTo;
490
491 int status = getStatus();
492
493 // tell just registered client my status, but do not specify cause
494 notifyOneClient(client, status, CAUSE_NOT_SPECIFIED);
495
496 mClients.add(client);
497 } catch (RemoteException e) {
498 // do nothing if we cannot send update to the client
499 e.printStackTrace();
500 }
501
502 break;
503 case DynamicAndroidClient.MSG_UNREGISTER_LISTENER:
504 mClients.remove(msg.replyTo);
505 break;
506 default:
507 // do nothing
508 }
509 }
510}