From 64aa78297fc2e8cb70350fc468cf49a3768af5a1 Mon Sep 17 00:00:00 2001 From: Po-Chien Hsueh Date: Sat, 12 Jan 2019 00:40:02 +0800 Subject: [PATCH] Add DynamicAndroidInstallationService DynamicAndroidInstallationService is a framework service to download, unzip and write DynamicAndroid images. Apps should interact with it using DynamicAndroidClient. Or, developers can use adb am commmands. Test: build and run on internal target Bug: 122015653 Change-Id: I7c834ed37de52840a407fb140743eda1f2bd82e8 --- data/etc/privapp-permissions-platform.xml | 5 + .../Android.mk | 19 + .../AndroidManifest.xml | 47 ++ .../MODULE_LICENSE_APACHE2 | 0 .../DynamicAndroidInstallationService/NOTICE | 190 +++++++ .../ic_system_update_googblue_24dp.xml | 9 + .../res/values/strings.xml | 33 ++ .../dynandroid/BootCompletedReceiver.java | 50 ++ .../DynamicAndroidInstallationService.java | 483 ++++++++++++++++++ .../dynandroid/InstallationAsyncTask.java | 205 ++++++++ .../dynandroid/VerificationActivity.java | 104 ++++ 11 files changed, 1145 insertions(+) create mode 100644 packages/DynamicAndroidInstallationService/Android.mk create mode 100644 packages/DynamicAndroidInstallationService/AndroidManifest.xml create mode 100644 packages/DynamicAndroidInstallationService/MODULE_LICENSE_APACHE2 create mode 100644 packages/DynamicAndroidInstallationService/NOTICE create mode 100644 packages/DynamicAndroidInstallationService/res/drawable/ic_system_update_googblue_24dp.xml create mode 100644 packages/DynamicAndroidInstallationService/res/values/strings.xml create mode 100644 packages/DynamicAndroidInstallationService/src/com/android/dynandroid/BootCompletedReceiver.java create mode 100644 packages/DynamicAndroidInstallationService/src/com/android/dynandroid/DynamicAndroidInstallationService.java create mode 100644 packages/DynamicAndroidInstallationService/src/com/android/dynandroid/InstallationAsyncTask.java create mode 100644 packages/DynamicAndroidInstallationService/src/com/android/dynandroid/VerificationActivity.java diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 904c3fb6d5499..ab7b2e2fea7ad 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -348,4 +348,9 @@ applications that come with the platform + + + + + diff --git a/packages/DynamicAndroidInstallationService/Android.mk b/packages/DynamicAndroidInstallationService/Android.mk new file mode 100644 index 0000000000000..13d96ac3906e6 --- /dev/null +++ b/packages/DynamicAndroidInstallationService/Android.mk @@ -0,0 +1,19 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := optional + +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, res) + +LOCAL_USE_AAPT2 := true + +LOCAL_PACKAGE_NAME := DynamicAndroidInstallationService +LOCAL_CERTIFICATE := platform +LOCAL_PRIVILEGED_MODULE := true +LOCAL_PRIVATE_PLATFORM_APIS := true + +LOCAL_PROGUARD_ENABLED := disabled + + +include $(BUILD_PACKAGE) diff --git a/packages/DynamicAndroidInstallationService/AndroidManifest.xml b/packages/DynamicAndroidInstallationService/AndroidManifest.xml new file mode 100644 index 0000000000000..1c1c72c462060 --- /dev/null +++ b/packages/DynamicAndroidInstallationService/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/DynamicAndroidInstallationService/MODULE_LICENSE_APACHE2 b/packages/DynamicAndroidInstallationService/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/DynamicAndroidInstallationService/NOTICE b/packages/DynamicAndroidInstallationService/NOTICE new file mode 100644 index 0000000000000..c5b1efa7aac76 --- /dev/null +++ b/packages/DynamicAndroidInstallationService/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-2008, The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/packages/DynamicAndroidInstallationService/res/drawable/ic_system_update_googblue_24dp.xml b/packages/DynamicAndroidInstallationService/res/drawable/ic_system_update_googblue_24dp.xml new file mode 100644 index 0000000000000..acf1567ab7fe0 --- /dev/null +++ b/packages/DynamicAndroidInstallationService/res/drawable/ic_system_update_googblue_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/DynamicAndroidInstallationService/res/values/strings.xml b/packages/DynamicAndroidInstallationService/res/values/strings.xml new file mode 100644 index 0000000000000..221e1d75b1332 --- /dev/null +++ b/packages/DynamicAndroidInstallationService/res/values/strings.xml @@ -0,0 +1,33 @@ + + + + AndroidOnTap Installer + + + AndroidOnTap Installer + + + AndroidOnTap Installer + + + Please enter your password and continue to AndroidOnTap installation + + + Installation is completed, you can reboot into the new installed system now. + + Installation is in progress. + + Installation Failed. + + We are running in AndroidOnTap. + + + Cancel + + Discard + + Uninstall + + Reboot + + diff --git a/packages/DynamicAndroidInstallationService/src/com/android/dynandroid/BootCompletedReceiver.java b/packages/DynamicAndroidInstallationService/src/com/android/dynandroid/BootCompletedReceiver.java new file mode 100644 index 0000000000000..dd1be897b2eae --- /dev/null +++ b/packages/DynamicAndroidInstallationService/src/com/android/dynandroid/BootCompletedReceiver.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dynandroid; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DynamicAndroidClient; +import android.content.Intent; +import android.os.UserHandle; +import android.util.Log; + + +/** + * A BoardcastReceiver waiting for ACTION_BOOT_COMPLETED and ask + * the service to display a notification if we are currently running + * in DynamicAndroid. + */ +public class BootCompletedReceiver extends BroadcastReceiver { + + private static final String TAG = "BootCompletedReceiver"; + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + + Log.d(TAG, "Broadcast received: " + action); + + if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { + Intent startServiceIntent = new Intent( + context, DynamicAndroidInstallationService.class); + + startServiceIntent.setAction(DynamicAndroidClient.ACTION_NOTIFY_IF_IN_USE); + context.startServiceAsUser(startServiceIntent, UserHandle.SYSTEM); + } + } +} diff --git a/packages/DynamicAndroidInstallationService/src/com/android/dynandroid/DynamicAndroidInstallationService.java b/packages/DynamicAndroidInstallationService/src/com/android/dynandroid/DynamicAndroidInstallationService.java new file mode 100644 index 0000000000000..7755cbc9ca3b2 --- /dev/null +++ b/packages/DynamicAndroidInstallationService/src/com/android/dynandroid/DynamicAndroidInstallationService.java @@ -0,0 +1,483 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dynandroid; + +import static android.content.DynamicAndroidClient.ACTION_NOTIFY_IF_IN_USE; +import static android.content.DynamicAndroidClient.ACTION_START_INSTALL; +import static android.content.DynamicAndroidClient.CAUSE_ERROR_EXCEPTION; +import static android.content.DynamicAndroidClient.CAUSE_ERROR_INVALID_URL; +import static android.content.DynamicAndroidClient.CAUSE_ERROR_IO; +import static android.content.DynamicAndroidClient.CAUSE_INSTALL_CANCELLED; +import static android.content.DynamicAndroidClient.CAUSE_INSTALL_COMPLETED; +import static android.content.DynamicAndroidClient.CAUSE_NOT_SPECIFIED; +import static android.content.DynamicAndroidClient.STATUS_IN_PROGRESS; +import static android.content.DynamicAndroidClient.STATUS_IN_USE; +import static android.content.DynamicAndroidClient.STATUS_NOT_STARTED; +import static android.content.DynamicAndroidClient.STATUS_READY; +import static android.os.AsyncTask.Status.FINISHED; +import static android.os.AsyncTask.Status.PENDING; +import static android.os.AsyncTask.Status.RUNNING; + +import static com.android.dynandroid.InstallationAsyncTask.RESULT_ERROR_EXCEPTION; +import static com.android.dynandroid.InstallationAsyncTask.RESULT_ERROR_INVALID_URL; +import static com.android.dynandroid.InstallationAsyncTask.RESULT_ERROR_IO; +import static com.android.dynandroid.InstallationAsyncTask.RESULT_OK; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.DynamicAndroidClient; +import android.content.Intent; +import android.os.Bundle; +import android.os.DynamicAndroidManager; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.PowerManager; +import android.os.RemoteException; +import android.util.Log; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +/** + * This class is the service in charge of DynamicAndroid installation. + * It also posts status to notification bar and wait for user's + * cancel and confirm commnands. + */ +public class DynamicAndroidInstallationService extends Service + implements InstallationAsyncTask.InstallStatusListener { + + private static final String TAG = "DynAndroidInstallationService"; + + /* + * Intent actions + */ + private static final String ACTION_CANCEL_INSTALL = + "com.android.dynandroid.ACTION_CANCEL_INSTALL"; + private static final String ACTION_REBOOT_TO_DYN_ANDROID = + "com.android.dynandroid.ACTION_REBOOT_TO_DYN_ANDROID"; + private static final String ACTION_REBOOT_TO_NORMAL = + "com.android.dynandroid.ACTION_REBOOT_TO_NORMAL"; + + /* + * For notification + */ + private static final String NOTIFICATION_CHANNEL_ID = "com.android.dynandroid"; + private static final int NOTIFICATION_ID = 1; + + /* + * IPC + */ + /** Keeps track of all current registered clients. */ + ArrayList mClients = new ArrayList<>(); + + /** Handler of incoming messages from clients. */ + final Messenger mMessenger = new Messenger(new IncomingHandler(this)); + + static class IncomingHandler extends Handler { + private final WeakReference mWeakService; + + IncomingHandler(DynamicAndroidInstallationService service) { + mWeakService = new WeakReference<>(service); + } + + @Override + public void handleMessage(Message msg) { + DynamicAndroidInstallationService service = mWeakService.get(); + + if (service != null) { + service.handleMessage(msg); + } + } + } + + private DynamicAndroidManager mDynAndroid; + private NotificationManager mNM; + + private long mSystemSize; + private long mInstalledSize; + private boolean mJustCancelledByUser; + + private PendingIntent mPiCancel; + private PendingIntent mPiRebootToDynamicAndroid; + private PendingIntent mPiUninstallAndReboot; + + private InstallationAsyncTask mInstallTask; + + + @Override + public void onCreate() { + super.onCreate(); + + prepareNotification(); + + mDynAndroid = (DynamicAndroidManager) getSystemService(Context.DYNAMIC_ANDROID_SERVICE); + } + + @Override + public void onDestroy() { + // Cancel the persistent notification. + mNM.cancel(NOTIFICATION_ID); + } + + @Override + public IBinder onBind(Intent intent) { + return mMessenger.getBinder(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + String action = intent.getAction(); + + Log.d(TAG, "onStartCommand(): action=" + action); + + if (ACTION_START_INSTALL.equals(action)) { + executeInstallCommand(intent); + } else if (ACTION_CANCEL_INSTALL.equals(action)) { + executeCancelCommand(); + } else if (ACTION_REBOOT_TO_DYN_ANDROID.equals(action)) { + executeRebootToDynAndroidCommand(); + } else if (ACTION_REBOOT_TO_NORMAL.equals(action)) { + executeRebootToNormalCommand(); + } else if (ACTION_NOTIFY_IF_IN_USE.equals(action)) { + executeNotifyIfInUseCommand(); + } + + return Service.START_NOT_STICKY; + } + + @Override + public void onProgressUpdate(long installedSize) { + mInstalledSize = installedSize; + postStatus(STATUS_IN_PROGRESS, CAUSE_NOT_SPECIFIED); + } + + @Override + public void onResult(int result) { + if (result == RESULT_OK) { + postStatus(STATUS_READY, CAUSE_INSTALL_COMPLETED); + return; + } + + // if it's not successful, reset the task and stop self. + resetTaskAndStop(); + + switch (result) { + case RESULT_ERROR_IO: + postStatus(STATUS_NOT_STARTED, CAUSE_ERROR_IO); + break; + + case RESULT_ERROR_INVALID_URL: + postStatus(STATUS_NOT_STARTED, CAUSE_ERROR_INVALID_URL); + break; + + case RESULT_ERROR_EXCEPTION: + postStatus(STATUS_NOT_STARTED, CAUSE_ERROR_EXCEPTION); + break; + } + } + + @Override + public void onCancelled() { + resetTaskAndStop(); + postStatus(STATUS_NOT_STARTED, CAUSE_INSTALL_CANCELLED); + } + + private void executeInstallCommand(Intent intent) { + if (!verifyRequest(intent)) { + Log.e(TAG, "Verification failed. Did you use VerificationActivity?"); + return; + } + + if (mInstallTask != null) { + Log.e(TAG, "There is already an install task running"); + return; + } + + if (isInDynamicAndroid()) { + Log.e(TAG, "We are already running in DynamicAndroid"); + return; + } + + String url = intent.getStringExtra(DynamicAndroidClient.KEY_SYSTEM_URL); + mSystemSize = intent.getLongExtra(DynamicAndroidClient.KEY_SYSTEM_SIZE, 0); + long userdata = intent.getLongExtra(DynamicAndroidClient.KEY_USERDATA_SIZE, 0); + + mInstallTask = new InstallationAsyncTask(url, mSystemSize, userdata, mDynAndroid, this); + mInstallTask.execute(); + + // start fore ground + startForeground(NOTIFICATION_ID, + buildNotification(STATUS_IN_PROGRESS, CAUSE_NOT_SPECIFIED)); + } + + private void executeCancelCommand() { + if (mInstallTask == null || mInstallTask.getStatus() == PENDING) { + Log.e(TAG, "Cancel command triggered, but there is no task running"); + mNM.cancel(NOTIFICATION_ID); + + return; + } + + mJustCancelledByUser = true; + + if (mInstallTask.cancel(false)) { + // Will cleanup and post status in onCancelled() + Log.d(TAG, "Cancel request filed successfully"); + } else { + Log.d(TAG, "Requested cancel, completed task will be discarded"); + + resetTaskAndStop(); + postStatus(STATUS_NOT_STARTED, CAUSE_INSTALL_CANCELLED); + } + + } + + private void executeRebootToDynAndroidCommand() { + if (mInstallTask == null || mInstallTask.getStatus() != FINISHED) { + Log.e(TAG, "Trying to reboot to DynamicAndroid, but there is no complete installation"); + return; + } + + if (!mInstallTask.commit()) { + // TODO: b/123673280 better UI response + Log.e(TAG, "Failed to commit installation because of native runtime error."); + mNM.cancel(NOTIFICATION_ID); + + return; + } + + PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); + + if (powerManager != null) { + powerManager.reboot("dynandroid"); + } + } + + private void executeRebootToNormalCommand() { + mDynAndroid.remove(); + + PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); + + if (powerManager != null) { + powerManager.reboot(null); + } + } + + private void executeNotifyIfInUseCommand() { + if (isInDynamicAndroid()) { + startForeground(NOTIFICATION_ID, + buildNotification(STATUS_IN_USE, CAUSE_NOT_SPECIFIED)); + } + } + + private void resetTaskAndStop() { + mInstallTask = null; + + stopForeground(true); + + // stop self, but this service is not destroyed yet if it's still bound + stopSelf(); + } + + private void prepareNotification() { + NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, + getString(R.string.notification_channel_name), + NotificationManager.IMPORTANCE_LOW); + + mNM = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + + if (mNM != null) { + mNM.createNotificationChannel(chan); + } + + Intent intentCancel = new Intent(this, DynamicAndroidInstallationService.class); + intentCancel.setAction(ACTION_CANCEL_INSTALL); + mPiCancel = PendingIntent.getService(this, 0, intentCancel, 0); + + Intent intentRebootToDyn = new Intent(this, DynamicAndroidInstallationService.class); + intentRebootToDyn.setAction(ACTION_REBOOT_TO_DYN_ANDROID); + mPiRebootToDynamicAndroid = PendingIntent.getService(this, 0, intentRebootToDyn, 0); + + Intent intentUninstallAndReboot = new Intent(this, DynamicAndroidInstallationService.class); + intentUninstallAndReboot.setAction(ACTION_REBOOT_TO_NORMAL); + mPiUninstallAndReboot = PendingIntent.getService(this, 0, intentUninstallAndReboot, 0); + } + + private Notification buildNotification(int status, int cause) { + Notification.Builder builder = new Notification.Builder(this, NOTIFICATION_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_system_update_googblue_24dp) + .setProgress(0, 0, false); + + switch (status) { + case STATUS_IN_PROGRESS: + builder.setContentText(getString(R.string.notification_install_inprogress)); + + int max = (int) Math.max(mSystemSize >> 20, 1); + int progress = (int) mInstalledSize >> 20; + + builder.setProgress(max, progress, false); + + builder.addAction(new Notification.Action.Builder( + null, getString(R.string.notification_action_cancel), + mPiCancel).build()); + + break; + + case STATUS_READY: + builder.setContentText(getString(R.string.notification_install_completed)); + + builder.addAction(new Notification.Action.Builder( + null, getString(R.string.notification_action_reboot_to_dynandroid), + mPiRebootToDynamicAndroid).build()); + + builder.addAction(new Notification.Action.Builder( + null, getString(R.string.notification_action_cancel), + mPiCancel).build()); + + break; + + case STATUS_IN_USE: + builder.setContentText(getString(R.string.notification_dynandroid_in_use)); + + builder.addAction(new Notification.Action.Builder( + null, getString(R.string.notification_action_uninstall), + mPiUninstallAndReboot).build()); + + break; + + case STATUS_NOT_STARTED: + if (cause != CAUSE_NOT_SPECIFIED && cause != CAUSE_INSTALL_CANCELLED) { + builder.setContentText(getString(R.string.notification_install_failed)); + } else { + // no need to notify the user if the task is not started, or cancelled. + } + break; + + default: + throw new IllegalStateException("status is invalid"); + } + + return builder.build(); + } + + private boolean verifyRequest(Intent intent) { + String url = intent.getStringExtra(DynamicAndroidClient.KEY_SYSTEM_URL); + + return VerificationActivity.isVerified(url); + } + + private void postStatus(int status, int cause) { + Log.d(TAG, "postStatus(): statusCode=" + status + ", causeCode=" + cause); + + boolean notifyOnNotificationBar = true; + + if (status == STATUS_NOT_STARTED + && cause == CAUSE_INSTALL_CANCELLED + && mJustCancelledByUser) { + // if task is cancelled by user, do not notify them + notifyOnNotificationBar = false; + mJustCancelledByUser = false; + } + + if (notifyOnNotificationBar) { + mNM.notify(NOTIFICATION_ID, buildNotification(status, cause)); + } + + for (int i = mClients.size() - 1; i >= 0; i--) { + try { + notifyOneClient(mClients.get(i), status, cause); + } catch (RemoteException e) { + mClients.remove(i); + } + } + } + + private void notifyOneClient(Messenger client, int status, int cause) throws RemoteException { + Bundle bundle = new Bundle(); + + bundle.putLong(DynamicAndroidClient.KEY_INSTALLED_SIZE, mInstalledSize); + + client.send(Message.obtain(null, + DynamicAndroidClient.MSG_POST_STATUS, status, cause, bundle)); + } + + private int getStatus() { + if (isInDynamicAndroid()) { + return STATUS_IN_USE; + + } else if (mInstallTask == null) { + return STATUS_NOT_STARTED; + + } + + switch (mInstallTask.getStatus()) { + case PENDING: + return STATUS_NOT_STARTED; + + case RUNNING: + return STATUS_IN_PROGRESS; + + case FINISHED: + int result = mInstallTask.getResult(); + + if (result == RESULT_OK) { + return STATUS_READY; + } else { + throw new IllegalStateException("A failed InstallationTask is not reset"); + } + + default: + return STATUS_NOT_STARTED; + } + } + + private boolean isInDynamicAndroid() { + return mDynAndroid.isInUse(); + } + + void handleMessage(Message msg) { + switch (msg.what) { + case DynamicAndroidClient.MSG_REGISTER_LISTENER: + try { + Messenger client = msg.replyTo; + + int status = getStatus(); + + // tell just registered client my status, but do not specify cause + notifyOneClient(client, status, CAUSE_NOT_SPECIFIED); + + mClients.add(client); + } catch (RemoteException e) { + // do nothing if we cannot send update to the client + e.printStackTrace(); + } + + break; + case DynamicAndroidClient.MSG_UNREGISTER_LISTENER: + mClients.remove(msg.replyTo); + break; + default: + // do nothing + } + } +} diff --git a/packages/DynamicAndroidInstallationService/src/com/android/dynandroid/InstallationAsyncTask.java b/packages/DynamicAndroidInstallationService/src/com/android/dynandroid/InstallationAsyncTask.java new file mode 100644 index 0000000000000..3c759e948b4ed --- /dev/null +++ b/packages/DynamicAndroidInstallationService/src/com/android/dynandroid/InstallationAsyncTask.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dynandroid; + +import android.os.AsyncTask; +import android.os.DynamicAndroidManager; +import android.util.Log; +import android.webkit.URLUtil; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Arrays; +import java.util.Locale; +import java.util.zip.GZIPInputStream; + + +class InstallationAsyncTask extends AsyncTask { + + private static final String TAG = "InstallationAsyncTask"; + + private static final int READ_BUFFER_SIZE = 1 << 19; + + private class InvalidImageUrlException extends RuntimeException { + private InvalidImageUrlException(String message) { + super(message); + } + } + + + /** Not completed, including being cancelled */ + static final int NO_RESULT = 0; + static final int RESULT_OK = 1; + static final int RESULT_ERROR_IO = 2; + static final int RESULT_ERROR_INVALID_URL = 3; + static final int RESULT_ERROR_EXCEPTION = 6; + + interface InstallStatusListener { + void onProgressUpdate(long installedSize); + void onResult(int resultCode); + void onCancelled(); + } + + private final String mUrl; + private final long mSystemSize; + private final long mUserdataSize; + private final DynamicAndroidManager mDynamicAndroid; + private final InstallStatusListener mListener; + private DynamicAndroidManager.Session mInstallationSession; + + private long mInstalledSize; + private long mReportedInstalledSize; + private int mResult = NO_RESULT; + + private InputStream mStream; + + + InstallationAsyncTask(String url, long systemSize, long userdataSize, + DynamicAndroidManager dynAndroid, InstallStatusListener listener) { + mUrl = url; + mSystemSize = systemSize; + mUserdataSize = userdataSize; + mDynamicAndroid = dynAndroid; + mListener = listener; + } + + @Override + protected void onPreExecute() { + mListener.onProgressUpdate(0); + } + + @Override + protected Integer doInBackground(String... voids) { + Log.d(TAG, "Start doInBackground(), URL: " + mUrl); + + try { + // call start in background + mInstallationSession = mDynamicAndroid.startInstallation(mSystemSize, mUserdataSize); + + if (mInstallationSession == null) { + Log.e(TAG, "Failed to start installation with requested size: " + + (mSystemSize + mUserdataSize)); + + return RESULT_ERROR_IO; + } + + initInputStream(); + + byte[] bytes = new byte[READ_BUFFER_SIZE]; + + int numBytesRead; + long minStepToReport = mSystemSize / 100; + + Log.d(TAG, "Start installation loop"); + while ((numBytesRead = mStream.read(bytes, 0, READ_BUFFER_SIZE)) != -1) { + if (isCancelled()) { + break; + } + + byte[] writeBuffer = numBytesRead == READ_BUFFER_SIZE + ? bytes : Arrays.copyOf(bytes, numBytesRead); + + if (!mInstallationSession.write(writeBuffer)) { + throw new IOException("Failed write() to DynamicAndroid"); + } + + mInstalledSize += numBytesRead; + + if (mInstalledSize > mReportedInstalledSize + minStepToReport) { + publishProgress(mInstalledSize); + mReportedInstalledSize = mInstalledSize; + } + } + + return RESULT_OK; + + } catch (IOException e) { + e.printStackTrace(); + return RESULT_ERROR_IO; + + } catch (InvalidImageUrlException e) { + e.printStackTrace(); + return RESULT_ERROR_INVALID_URL; + + } catch (Exception e) { + e.printStackTrace(); + return RESULT_ERROR_EXCEPTION; + + } finally { + close(); + } + } + + @Override + protected void onCancelled() { + Log.d(TAG, "onCancelled(), URL: " + mUrl); + + close(); + + mListener.onCancelled(); + } + + @Override + protected void onPostExecute(Integer result) { + Log.d(TAG, "onPostExecute(), URL: " + mUrl + ", result: " + result); + + close(); + + mResult = result; + mListener.onResult(mResult); + } + + @Override + protected void onProgressUpdate(Long... values) { + long progress = values[0]; + mListener.onProgressUpdate(progress); + } + + private void initInputStream() throws IOException, InvalidImageUrlException { + if (URLUtil.isNetworkUrl(mUrl) || URLUtil.isFileUrl(mUrl)) { + mStream = new BufferedInputStream(new GZIPInputStream(new URL(mUrl).openStream())); + } else { + throw new InvalidImageUrlException( + String.format(Locale.US, "Unsupported file source: %s", mUrl)); + } + } + + private void close() { + try { + if (mStream != null) { + mStream.close(); + mStream = null; + } + } catch (IOException e) { + // ignore + } + } + + int getResult() { + return mResult; + } + + boolean commit() { + if (mInstallationSession == null) { + return false; + } + + return mInstallationSession.commit(); + } +} diff --git a/packages/DynamicAndroidInstallationService/src/com/android/dynandroid/VerificationActivity.java b/packages/DynamicAndroidInstallationService/src/com/android/dynandroid/VerificationActivity.java new file mode 100644 index 0000000000000..c18c4fe689c9e --- /dev/null +++ b/packages/DynamicAndroidInstallationService/src/com/android/dynandroid/VerificationActivity.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dynandroid; + +import static android.content.DynamicAndroidClient.KEY_SYSTEM_SIZE; +import static android.content.DynamicAndroidClient.KEY_SYSTEM_URL; +import static android.content.DynamicAndroidClient.KEY_USERDATA_SIZE; + +import android.app.Activity; +import android.app.KeyguardManager; +import android.content.Context; +import android.content.DynamicAndroidClient; +import android.content.Intent; +import android.os.Bundle; +import android.os.UserHandle; +import android.util.Log; + + +/** + * This Activity starts KeyguardManager and ask the user to confirm + * before any installation request. If the device is not protected by + * a password, it approves the request by default. + */ +public class VerificationActivity extends Activity { + + private static final String TAG = "VerificationActivity"; + + private static final int REQUEST_CODE = 1; + + // For install request verification + private static String sVerifiedUrl; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); + + if (km != null) { + String title = getString(R.string.keyguard_title); + String description = getString(R.string.keyguard_description); + Intent intent = km.createConfirmDeviceCredentialIntent(title, description); + + if (intent == null) { + Log.d(TAG, "This device is not protected by a password/pin"); + startInstallationService(); + finish(); + } else { + startActivityForResult(intent, REQUEST_CODE); + } + } else { + finish(); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) { + startInstallationService(); + } + + finish(); + } + + private void startInstallationService() { + // retrieve data from calling intent + Intent callingIntent = getIntent(); + + String url = callingIntent.getStringExtra(KEY_SYSTEM_URL); + long systemSize = callingIntent.getLongExtra(KEY_SYSTEM_SIZE, 0); + long userdataSize = callingIntent.getLongExtra(KEY_USERDATA_SIZE, 0); + + sVerifiedUrl = url; + + // start service + Intent intent = new Intent(this, DynamicAndroidInstallationService.class); + intent.setAction(DynamicAndroidClient.ACTION_START_INSTALL); + intent.putExtra(KEY_SYSTEM_URL, url); + intent.putExtra(KEY_SYSTEM_SIZE, systemSize); + intent.putExtra(KEY_USERDATA_SIZE, userdataSize); + + Log.d(TAG, "Starting Installation Service"); + startServiceAsUser(intent, UserHandle.SYSTEM); + } + + static boolean isVerified(String url) { + return sVerifiedUrl != null && sVerifiedUrl.equals(url); + } +}