blob: 7755cbc9ca3b2d49f003e55dc4bade228b31944d [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;
57
58import java.lang.ref.WeakReference;
59import java.util.ArrayList;
60
61/**
62 * This class is the service in charge of DynamicAndroid installation.
63 * It also posts status to notification bar and wait for user's
64 * cancel and confirm commnands.
65 */
66public class DynamicAndroidInstallationService extends Service
67 implements InstallationAsyncTask.InstallStatusListener {
68
69 private static final String TAG = "DynAndroidInstallationService";
70
71 /*
72 * Intent actions
73 */
74 private static final String ACTION_CANCEL_INSTALL =
75 "com.android.dynandroid.ACTION_CANCEL_INSTALL";
76 private static final String ACTION_REBOOT_TO_DYN_ANDROID =
77 "com.android.dynandroid.ACTION_REBOOT_TO_DYN_ANDROID";
78 private static final String ACTION_REBOOT_TO_NORMAL =
79 "com.android.dynandroid.ACTION_REBOOT_TO_NORMAL";
80
81 /*
82 * For notification
83 */
84 private static final String NOTIFICATION_CHANNEL_ID = "com.android.dynandroid";
85 private static final int NOTIFICATION_ID = 1;
86
87 /*
88 * IPC
89 */
90 /** Keeps track of all current registered clients. */
91 ArrayList<Messenger> mClients = new ArrayList<>();
92
93 /** Handler of incoming messages from clients. */
94 final Messenger mMessenger = new Messenger(new IncomingHandler(this));
95
96 static class IncomingHandler extends Handler {
97 private final WeakReference<DynamicAndroidInstallationService> mWeakService;
98
99 IncomingHandler(DynamicAndroidInstallationService service) {
100 mWeakService = new WeakReference<>(service);
101 }
102
103 @Override
104 public void handleMessage(Message msg) {
105 DynamicAndroidInstallationService service = mWeakService.get();
106
107 if (service != null) {
108 service.handleMessage(msg);
109 }
110 }
111 }
112
113 private DynamicAndroidManager mDynAndroid;
114 private NotificationManager mNM;
115
116 private long mSystemSize;
117 private long mInstalledSize;
118 private boolean mJustCancelledByUser;
119
120 private PendingIntent mPiCancel;
121 private PendingIntent mPiRebootToDynamicAndroid;
122 private PendingIntent mPiUninstallAndReboot;
123
124 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();
157 } else if (ACTION_REBOOT_TO_DYN_ANDROID.equals(action)) {
158 executeRebootToDynAndroidCommand();
159 } else if (ACTION_REBOOT_TO_NORMAL.equals(action)) {
160 executeRebootToNormalCommand();
161 } else if (ACTION_NOTIFY_IF_IN_USE.equals(action)) {
162 executeNotifyIfInUseCommand();
163 }
164
165 return Service.START_NOT_STICKY;
166 }
167
168 @Override
169 public void onProgressUpdate(long installedSize) {
170 mInstalledSize = installedSize;
171 postStatus(STATUS_IN_PROGRESS, CAUSE_NOT_SPECIFIED);
172 }
173
174 @Override
175 public void onResult(int result) {
176 if (result == RESULT_OK) {
177 postStatus(STATUS_READY, CAUSE_INSTALL_COMPLETED);
178 return;
179 }
180
181 // if it's not successful, reset the task and stop self.
182 resetTaskAndStop();
183
184 switch (result) {
185 case RESULT_ERROR_IO:
186 postStatus(STATUS_NOT_STARTED, CAUSE_ERROR_IO);
187 break;
188
189 case RESULT_ERROR_INVALID_URL:
190 postStatus(STATUS_NOT_STARTED, CAUSE_ERROR_INVALID_URL);
191 break;
192
193 case RESULT_ERROR_EXCEPTION:
194 postStatus(STATUS_NOT_STARTED, CAUSE_ERROR_EXCEPTION);
195 break;
196 }
197 }
198
199 @Override
200 public void onCancelled() {
201 resetTaskAndStop();
202 postStatus(STATUS_NOT_STARTED, CAUSE_INSTALL_CANCELLED);
203 }
204
205 private void executeInstallCommand(Intent intent) {
206 if (!verifyRequest(intent)) {
207 Log.e(TAG, "Verification failed. Did you use VerificationActivity?");
208 return;
209 }
210
211 if (mInstallTask != null) {
212 Log.e(TAG, "There is already an install task running");
213 return;
214 }
215
216 if (isInDynamicAndroid()) {
217 Log.e(TAG, "We are already running in DynamicAndroid");
218 return;
219 }
220
221 String url = intent.getStringExtra(DynamicAndroidClient.KEY_SYSTEM_URL);
222 mSystemSize = intent.getLongExtra(DynamicAndroidClient.KEY_SYSTEM_SIZE, 0);
223 long userdata = intent.getLongExtra(DynamicAndroidClient.KEY_USERDATA_SIZE, 0);
224
225 mInstallTask = new InstallationAsyncTask(url, mSystemSize, userdata, mDynAndroid, this);
226 mInstallTask.execute();
227
228 // start fore ground
229 startForeground(NOTIFICATION_ID,
230 buildNotification(STATUS_IN_PROGRESS, CAUSE_NOT_SPECIFIED));
231 }
232
233 private void executeCancelCommand() {
234 if (mInstallTask == null || mInstallTask.getStatus() == PENDING) {
235 Log.e(TAG, "Cancel command triggered, but there is no task running");
236 mNM.cancel(NOTIFICATION_ID);
237
238 return;
239 }
240
241 mJustCancelledByUser = true;
242
243 if (mInstallTask.cancel(false)) {
244 // Will cleanup and post status in onCancelled()
245 Log.d(TAG, "Cancel request filed successfully");
246 } else {
247 Log.d(TAG, "Requested cancel, completed task will be discarded");
248
249 resetTaskAndStop();
250 postStatus(STATUS_NOT_STARTED, CAUSE_INSTALL_CANCELLED);
251 }
252
253 }
254
255 private void executeRebootToDynAndroidCommand() {
256 if (mInstallTask == null || mInstallTask.getStatus() != FINISHED) {
257 Log.e(TAG, "Trying to reboot to DynamicAndroid, but there is no complete installation");
258 return;
259 }
260
261 if (!mInstallTask.commit()) {
262 // TODO: b/123673280 better UI response
263 Log.e(TAG, "Failed to commit installation because of native runtime error.");
264 mNM.cancel(NOTIFICATION_ID);
265
266 return;
267 }
268
269 PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
270
271 if (powerManager != null) {
272 powerManager.reboot("dynandroid");
273 }
274 }
275
276 private void executeRebootToNormalCommand() {
277 mDynAndroid.remove();
278
279 PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
280
281 if (powerManager != null) {
282 powerManager.reboot(null);
283 }
284 }
285
286 private void executeNotifyIfInUseCommand() {
287 if (isInDynamicAndroid()) {
288 startForeground(NOTIFICATION_ID,
289 buildNotification(STATUS_IN_USE, CAUSE_NOT_SPECIFIED));
290 }
291 }
292
293 private void resetTaskAndStop() {
294 mInstallTask = null;
295
296 stopForeground(true);
297
298 // stop self, but this service is not destroyed yet if it's still bound
299 stopSelf();
300 }
301
302 private void prepareNotification() {
303 NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID,
304 getString(R.string.notification_channel_name),
305 NotificationManager.IMPORTANCE_LOW);
306
307 mNM = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
308
309 if (mNM != null) {
310 mNM.createNotificationChannel(chan);
311 }
312
313 Intent intentCancel = new Intent(this, DynamicAndroidInstallationService.class);
314 intentCancel.setAction(ACTION_CANCEL_INSTALL);
315 mPiCancel = PendingIntent.getService(this, 0, intentCancel, 0);
316
317 Intent intentRebootToDyn = new Intent(this, DynamicAndroidInstallationService.class);
318 intentRebootToDyn.setAction(ACTION_REBOOT_TO_DYN_ANDROID);
319 mPiRebootToDynamicAndroid = PendingIntent.getService(this, 0, intentRebootToDyn, 0);
320
321 Intent intentUninstallAndReboot = new Intent(this, DynamicAndroidInstallationService.class);
322 intentUninstallAndReboot.setAction(ACTION_REBOOT_TO_NORMAL);
323 mPiUninstallAndReboot = PendingIntent.getService(this, 0, intentUninstallAndReboot, 0);
324 }
325
326 private Notification buildNotification(int status, int cause) {
327 Notification.Builder builder = new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
328 .setSmallIcon(R.drawable.ic_system_update_googblue_24dp)
329 .setProgress(0, 0, false);
330
331 switch (status) {
332 case STATUS_IN_PROGRESS:
333 builder.setContentText(getString(R.string.notification_install_inprogress));
334
335 int max = (int) Math.max(mSystemSize >> 20, 1);
336 int progress = (int) mInstalledSize >> 20;
337
338 builder.setProgress(max, progress, false);
339
340 builder.addAction(new Notification.Action.Builder(
341 null, getString(R.string.notification_action_cancel),
342 mPiCancel).build());
343
344 break;
345
346 case STATUS_READY:
347 builder.setContentText(getString(R.string.notification_install_completed));
348
349 builder.addAction(new Notification.Action.Builder(
350 null, getString(R.string.notification_action_reboot_to_dynandroid),
351 mPiRebootToDynamicAndroid).build());
352
353 builder.addAction(new Notification.Action.Builder(
354 null, getString(R.string.notification_action_cancel),
355 mPiCancel).build());
356
357 break;
358
359 case STATUS_IN_USE:
360 builder.setContentText(getString(R.string.notification_dynandroid_in_use));
361
362 builder.addAction(new Notification.Action.Builder(
363 null, getString(R.string.notification_action_uninstall),
364 mPiUninstallAndReboot).build());
365
366 break;
367
368 case STATUS_NOT_STARTED:
369 if (cause != CAUSE_NOT_SPECIFIED && cause != CAUSE_INSTALL_CANCELLED) {
370 builder.setContentText(getString(R.string.notification_install_failed));
371 } else {
372 // no need to notify the user if the task is not started, or cancelled.
373 }
374 break;
375
376 default:
377 throw new IllegalStateException("status is invalid");
378 }
379
380 return builder.build();
381 }
382
383 private boolean verifyRequest(Intent intent) {
384 String url = intent.getStringExtra(DynamicAndroidClient.KEY_SYSTEM_URL);
385
386 return VerificationActivity.isVerified(url);
387 }
388
389 private void postStatus(int status, int cause) {
390 Log.d(TAG, "postStatus(): statusCode=" + status + ", causeCode=" + cause);
391
392 boolean notifyOnNotificationBar = true;
393
394 if (status == STATUS_NOT_STARTED
395 && cause == CAUSE_INSTALL_CANCELLED
396 && mJustCancelledByUser) {
397 // if task is cancelled by user, do not notify them
398 notifyOnNotificationBar = false;
399 mJustCancelledByUser = false;
400 }
401
402 if (notifyOnNotificationBar) {
403 mNM.notify(NOTIFICATION_ID, buildNotification(status, cause));
404 }
405
406 for (int i = mClients.size() - 1; i >= 0; i--) {
407 try {
408 notifyOneClient(mClients.get(i), status, cause);
409 } catch (RemoteException e) {
410 mClients.remove(i);
411 }
412 }
413 }
414
415 private void notifyOneClient(Messenger client, int status, int cause) throws RemoteException {
416 Bundle bundle = new Bundle();
417
418 bundle.putLong(DynamicAndroidClient.KEY_INSTALLED_SIZE, mInstalledSize);
419
420 client.send(Message.obtain(null,
421 DynamicAndroidClient.MSG_POST_STATUS, status, cause, bundle));
422 }
423
424 private int getStatus() {
425 if (isInDynamicAndroid()) {
426 return STATUS_IN_USE;
427
428 } else if (mInstallTask == null) {
429 return STATUS_NOT_STARTED;
430
431 }
432
433 switch (mInstallTask.getStatus()) {
434 case PENDING:
435 return STATUS_NOT_STARTED;
436
437 case RUNNING:
438 return STATUS_IN_PROGRESS;
439
440 case FINISHED:
441 int result = mInstallTask.getResult();
442
443 if (result == RESULT_OK) {
444 return STATUS_READY;
445 } else {
446 throw new IllegalStateException("A failed InstallationTask is not reset");
447 }
448
449 default:
450 return STATUS_NOT_STARTED;
451 }
452 }
453
454 private boolean isInDynamicAndroid() {
455 return mDynAndroid.isInUse();
456 }
457
458 void handleMessage(Message msg) {
459 switch (msg.what) {
460 case DynamicAndroidClient.MSG_REGISTER_LISTENER:
461 try {
462 Messenger client = msg.replyTo;
463
464 int status = getStatus();
465
466 // tell just registered client my status, but do not specify cause
467 notifyOneClient(client, status, CAUSE_NOT_SPECIFIED);
468
469 mClients.add(client);
470 } catch (RemoteException e) {
471 // do nothing if we cannot send update to the client
472 e.printStackTrace();
473 }
474
475 break;
476 case DynamicAndroidClient.MSG_UNREGISTER_LISTENER:
477 mClients.remove(msg.replyTo);
478 break;
479 default:
480 // do nothing
481 }
482 }
483}