471 lines
16 KiB
Java
471 lines
16 KiB
Java
![]() |
/*
|
||
|
* 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;
|
||
|
|
||
|
/**
|
||
|
* <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}, 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.</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
|
||
|
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<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, "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
|
||
|
|
||
|
}
|
||
|
}
|
||
|
}
|