| /* |
| * 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.annotation.TestApi; |
| 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.os.SystemProperties; |
| import android.util.FeatureFlagUtils; |
| import android.util.Slog; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.lang.ref.WeakReference; |
| import java.util.concurrent.Executor; |
| |
| /** |
| * <p>This class contains methods and constants used to start a {@code DynamicSystem} installation, |
| * and a listener for status updates.</p> |
| * |
| * <p>{@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.</p> |
| * |
| * 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}, persitent 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.</p> |
| * |
| * <p>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.</p> |
| * |
| * @hide |
| */ |
| @SystemApi |
| @TestApi |
| public class 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 {} |
| |
| private static final String TAG = "DynSystemClient"; |
| |
| private static final long DEFAULT_USERDATA_SIZE = (10L << 30); |
| |
| |
| /** 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 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"; |
| |
| |
| private static class IncomingHandler extends Handler { |
| private final WeakReference<DynamicSystemClient> 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, "DynSystemService connected"); |
| |
| 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"); |
| if (mExecutor != null) { |
| mExecutor.execute(() -> { |
| mListener.onStatusChanged(STATUS_UNKNOWN, CAUSE_ERROR_IPC, 0, e); |
| }); |
| } else { |
| mListener.onStatusChanged(STATUS_UNKNOWN, CAUSE_ERROR_IPC, 0, e); |
| } |
| } |
| } |
| |
| public void onServiceDisconnected(ComponentName className) { |
| Slog.v(TAG, "DynSystemService disconnected"); |
| 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 |
| @TestApi |
| 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; |
| } |
| |
| /** |
| * 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 |
| @TestApi |
| public void bind() { |
| if (!featureFlagEnabled()) { |
| Slog.w(TAG, FeatureFlagUtils.DYNAMIC_SYSTEM + " not enabled; bind() aborted."); |
| return; |
| } |
| |
| 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 |
| @TestApi |
| 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 |
| @TestApi |
| public void start(@NonNull Uri systemUrl, @BytesLong long systemSize) { |
| start(systemUrl, systemSize, 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) { |
| if (!featureFlagEnabled()) { |
| Slog.w(TAG, FeatureFlagUtils.DYNAMIC_SYSTEM + " not enabled; start() aborted."); |
| return; |
| } |
| |
| Intent intent = new Intent(); |
| |
| intent.setClassName("com.android.dynsystem", |
| "com.android.dynsystem.VerificationActivity"); |
| |
| intent.setData(systemUrl); |
| intent.setAction(ACTION_START_INSTALL); |
| |
| intent.putExtra(KEY_SYSTEM_SIZE, systemSize); |
| intent.putExtra(KEY_USERDATA_SIZE, userdataSize); |
| |
| mContext.startActivity(intent); |
| } |
| |
| private boolean featureFlagEnabled() { |
| return SystemProperties.getBoolean( |
| FeatureFlagUtils.PERSIST_PREFIX + FeatureFlagUtils.DYNAMIC_SYSTEM, false); |
| } |
| |
| 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); |
| |
| Throwable detail = t == null ? null : t.getCause(); |
| |
| if (mExecutor != null) { |
| mExecutor.execute(() -> { |
| mListener.onStatusChanged(status, cause, progress, detail); |
| }); |
| } else { |
| mListener.onStatusChanged(status, cause, progress, detail); |
| } |
| break; |
| default: |
| // do nothing |
| |
| } |
| } |
| } |