/* * 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 android.os.image; import android.annotation.BytesLong; import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.Messenger; import android.os.ParcelableException; import android.os.RemoteException; import android.util.Slog; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.util.concurrent.Executor; /** *

This class contains methods and constants used to start a {@code DynamicSystem} installation, * and a listener for status updates.

* *

{@code DynamicSystem} allows users to run certified system images in a non destructive manner * without needing to prior OEM unlock. It creates a temporary system partition to install the new * system image, and a temporary data partition for the newly installed system to run with.

* * After the installation is completed, the device will be running in the new system on next the * reboot. Then, when the user reboots the device again, it will leave {@code DynamicSystem} and go * back to the original system. While running in {@code DynamicSystem}, persistent storage for * factory reset protection (FRP) remains unchanged. Since the user is running the new system with * a temporarily created data partition, their original user data are kept unchanged.

* *

With {@link #setOnStatusChangedListener}, API users can register an * {@link #OnStatusChangedListener} to get status updates and their causes when the installation is * started, stopped, or cancelled. It also sends progress updates during the installation. With * {@link #start}, API users can start an installation with the {@link Uri} to a unsparsed and * gzipped system image. The {@link Uri} can be a web URL or a content Uri to a local path.

* * @hide */ @SystemApi public class DynamicSystemClient { private static final String TAG = "DynamicSystemClient"; /** @hide */ @IntDef(prefix = { "STATUS_" }, value = { STATUS_UNKNOWN, STATUS_NOT_STARTED, STATUS_IN_PROGRESS, STATUS_READY, STATUS_IN_USE, }) @Retention(RetentionPolicy.SOURCE) public @interface InstallationStatus {} /** @hide */ @IntDef(prefix = { "CAUSE_" }, value = { CAUSE_NOT_SPECIFIED, CAUSE_INSTALL_COMPLETED, CAUSE_INSTALL_CANCELLED, CAUSE_ERROR_IO, CAUSE_ERROR_INVALID_URL, CAUSE_ERROR_IPC, CAUSE_ERROR_EXCEPTION, }) @Retention(RetentionPolicy.SOURCE) public @interface StatusChangedCause {} /** Listener for installation status updates. */ public interface OnStatusChangedListener { /** * This callback is called when installation status is changed, and when the * client is {@link #bind} to {@code DynamicSystem} installation service. * * @param status status code, also defined in {@code DynamicSystemClient}. * @param cause cause code, also defined in {@code DynamicSystemClient}. * @param progress number of bytes installed. * @param detail additional detail about the error if available, otherwise null. */ void onStatusChanged(@InstallationStatus int status, @StatusChangedCause int cause, @BytesLong long progress, @Nullable Throwable detail); } /* * Status codes */ /** We are bound to installation service, but failed to get its status */ public static final int STATUS_UNKNOWN = 0; /** Installation is not started yet. */ public static final int STATUS_NOT_STARTED = 1; /** Installation is in progress. */ public static final int STATUS_IN_PROGRESS = 2; /** Installation is finished but the user has not launched it. */ public static final int STATUS_READY = 3; /** Device is running in {@code DynamicSystem}. */ public static final int STATUS_IN_USE = 4; /* * Causes */ /** Cause is not specified. This means the status is not changed. */ public static final int CAUSE_NOT_SPECIFIED = 0; /** Status changed because installation is completed. */ public static final int CAUSE_INSTALL_COMPLETED = 1; /** Status changed because installation is cancelled. */ public static final int CAUSE_INSTALL_CANCELLED = 2; /** Installation failed due to {@code IOException}. */ public static final int CAUSE_ERROR_IO = 3; /** Installation failed because the image URL source is not supported. */ public static final int CAUSE_ERROR_INVALID_URL = 4; /** Installation failed due to IPC error. */ public static final int CAUSE_ERROR_IPC = 5; /** Installation failed due to unhandled exception. */ public static final int CAUSE_ERROR_EXCEPTION = 6; /* * IPC Messages */ /** * Message to register listener. * @hide */ public static final int MSG_REGISTER_LISTENER = 1; /** * Message to unregister listener. * @hide */ public static final int MSG_UNREGISTER_LISTENER = 2; /** * Message for status updates. * @hide */ public static final int MSG_POST_STATUS = 3; /* * Messages keys */ /** * Message key, for progress updates. * @hide */ public static final String KEY_INSTALLED_SIZE = "KEY_INSTALLED_SIZE"; /** * Message key, used when the service is sending exception detail to the client. * @hide */ public static final String KEY_EXCEPTION_DETAIL = "KEY_EXCEPTION_DETAIL"; /* * Intent Actions */ /** * Intent action: start installation. * @hide */ public static final String ACTION_START_INSTALL = "android.os.image.action.START_INSTALL"; /** * Intent action: notify user if we are currently running in {@code DynamicSystem}. * @hide */ public static final String ACTION_NOTIFY_IF_IN_USE = "android.os.image.action.NOTIFY_IF_IN_USE"; /** * Intent action: hide notifications about the status of {@code DynamicSystem}. * @hide */ public static final String ACTION_HIDE_NOTIFICATION = "android.os.image.action.HIDE_NOTIFICATION"; /** * Intent action: notify the service to post a status update when keyguard is dismissed. * @hide */ public static final String ACTION_NOTIFY_KEYGUARD_DISMISSED = "android.os.image.action.NOTIFY_KEYGUARD_DISMISSED"; /* * Intent Keys */ /** * Intent key: Size of the system image, in bytes. * @hide */ public static final String KEY_SYSTEM_SIZE = "KEY_SYSTEM_SIZE"; /** * Intent key: Number of bytes to reserve for userdata. * @hide */ public static final String KEY_USERDATA_SIZE = "KEY_USERDATA_SIZE"; /** * Intent key: Whether to enable DynamicSystem immediately after installation is done. * Note this will reboot the device automatically. * @hide */ public static final String KEY_ENABLE_WHEN_COMPLETED = "KEY_ENABLE_WHEN_COMPLETED"; /** * Intent key: Whether to leave DynamicSystem on device reboot. * False indicates a sticky mode where device stays in DynamicSystem across reboots. * @hide */ public static final String KEY_ONE_SHOT = "KEY_ONE_SHOT"; /** * Intent key: Whether to use default strings when showing the dialog that prompts * user for device credentials. * False indicates using the custom strings provided by {@code DynamicSystem}. * @hide */ public static final String KEY_KEYGUARD_USE_DEFAULT_STRINGS = "KEY_KEYGUARD_USE_DEFAULT_STRINGS"; private static class IncomingHandler extends Handler { private final WeakReference mWeakClient; IncomingHandler(DynamicSystemClient service) { super(Looper.getMainLooper()); mWeakClient = new WeakReference<>(service); } @Override public void handleMessage(Message msg) { DynamicSystemClient service = mWeakClient.get(); if (service != null) { service.handleMessage(msg); } } } private class DynSystemServiceConnection implements ServiceConnection { public void onServiceConnected(ComponentName className, IBinder service) { Slog.v(TAG, "onServiceConnected: " + className); mService = new Messenger(service); try { Message msg = Message.obtain(null, MSG_REGISTER_LISTENER); msg.replyTo = mMessenger; mService.send(msg); } catch (RemoteException e) { Slog.e(TAG, "Unable to get status from installation service"); notifyOnStatusChangedListener(STATUS_UNKNOWN, CAUSE_ERROR_IPC, 0, e); } } public void onServiceDisconnected(ComponentName className) { Slog.v(TAG, "onServiceDisconnected: " + className); mService = null; } } private final Context mContext; private final DynSystemServiceConnection mConnection; private final Messenger mMessenger; private boolean mBound; private Executor mExecutor; private OnStatusChangedListener mListener; private Messenger mService; /** * Create a new {@code DynamicSystem} client. * * @param context a {@link Context} will be used to bind the installation service. * * @hide */ @SystemApi public DynamicSystemClient(@NonNull Context context) { mContext = context; mConnection = new DynSystemServiceConnection(); mMessenger = new Messenger(new IncomingHandler(this)); } /** * This method register a listener for status change. The listener is called using * the executor. */ public void setOnStatusChangedListener( @NonNull @CallbackExecutor Executor executor, @NonNull OnStatusChangedListener listener) { mListener = listener; mExecutor = executor; } /** * This method register a listener for status change. The listener is called in main * thread. */ public void setOnStatusChangedListener( @NonNull OnStatusChangedListener listener) { mListener = listener; mExecutor = null; } private void notifyOnStatusChangedListener( int status, int cause, long progress, Throwable detail) { if (mListener != null) { if (mExecutor != null) { mExecutor.execute( () -> { mListener.onStatusChanged(status, cause, progress, detail); }); } else { mListener.onStatusChanged(status, cause, progress, detail); } } } /** * Bind to {@code DynamicSystem} installation service. Binding to the installation service * allows it to send status updates to {@link #OnStatusChangedListener}. It is recommanded * to bind before calling {@link #start} and get status updates. * @hide */ @RequiresPermission(android.Manifest.permission.INSTALL_DYNAMIC_SYSTEM) @SystemApi public void bind() { Intent intent = new Intent(); intent.setClassName("com.android.dynsystem", "com.android.dynsystem.DynamicSystemInstallationService"); mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE); mBound = true; } /** * Unbind from {@code DynamicSystem} installation service. Unbinding from the installation * service stops it from sending following status updates. * @hide */ @RequiresPermission(android.Manifest.permission.INSTALL_DYNAMIC_SYSTEM) @SystemApi public void unbind() { if (!mBound) { return; } if (mService != null) { try { Message msg = Message.obtain(null, MSG_UNREGISTER_LISTENER); msg.replyTo = mMessenger; mService.send(msg); } catch (RemoteException e) { Slog.e(TAG, "Unable to unregister from installation service"); } } // Detach our existing connection. mContext.unbindService(mConnection); mBound = false; } /** * Start installing {@code DynamicSystem} from URL with default userdata size. * * Calling this function will first start an Activity to confirm device credential, using * {@link KeyguardManager}. If it's confirmed, the installation service will be started. * * This function doesn't require prior calling {@link #bind}. * * @param systemUrl a network Uri, a file Uri or a content Uri pointing to a system image file. * @param systemSize size of system image. * @hide */ @RequiresPermission(android.Manifest.permission.INSTALL_DYNAMIC_SYSTEM) @SystemApi public void start(@NonNull Uri systemUrl, @BytesLong long systemSize) { start(systemUrl, systemSize, 0 /* Use the default userdata size */); } /** * Start installing {@code DynamicSystem} from URL. * * Calling this function will first start an Activity to confirm device credential, using * {@link KeyguardManager}. If it's confirmed, the installation service will be started. * * This function doesn't require prior calling {@link #bind}. * * @param systemUrl a network Uri, a file Uri or a content Uri pointing to a system image file. * @param systemSize size of system image. * @param userdataSize bytes reserved for userdata. */ @RequiresPermission(android.Manifest.permission.INSTALL_DYNAMIC_SYSTEM) public void start(@NonNull Uri systemUrl, @BytesLong long systemSize, @BytesLong long userdataSize) { Intent intent = new Intent(); intent.setClassName("com.android.dynsystem", "com.android.dynsystem.VerificationActivity"); intent.setData(systemUrl); intent.setAction(ACTION_START_INSTALL); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(KEY_SYSTEM_SIZE, systemSize); intent.putExtra(KEY_USERDATA_SIZE, userdataSize); mContext.startActivity(intent); } private void handleMessage(Message msg) { switch (msg.what) { case MSG_POST_STATUS: int status = msg.arg1; int cause = msg.arg2; // obj is non-null Bundle bundle = (Bundle) msg.obj; long progress = bundle.getLong(KEY_INSTALLED_SIZE); ParcelableException t = (ParcelableException) bundle.getSerializable( KEY_EXCEPTION_DETAIL, android.os.ParcelableException.class); Throwable detail = t == null ? null : t.getCause(); notifyOnStatusChangedListener(status, cause, progress, detail); break; default: // do nothing } } }