package com.lody.virtual.server.pm.installer;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.IPackageInstallerCallback;
import android.content.pm.IPackageInstallerSession;
import android.content.pm.PackageInstaller;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.SparseArray;
import com.lody.virtual.client.core.VirtualCore;
import com.lody.virtual.helper.compat.ObjectsCompat;
import com.lody.virtual.helper.utils.Singleton;
import com.lody.virtual.os.VBinder;
import com.lody.virtual.os.VEnvironment;
import com.lody.virtual.os.VUserHandle;
import com.lody.virtual.remote.VParceledListSlice;
import com.lody.virtual.server.IPackageInstaller;
import com.lody.virtual.server.pm.VAppManagerService;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import static com.lody.virtual.server.pm.installer.PackageHelper.installStatusToPublicStatus;
import static com.lody.virtual.server.pm.installer.PackageHelper.installStatusToString;
/**
* @author Lody
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class VPackageInstallerService extends IPackageInstaller.Stub {
private static final String TAG = "PackageInstaller";
/**
* Upper bound on number of active sessions for a UID
*/
private static final long MAX_ACTIVE_SESSIONS = 1024;
private static final Singleton<VPackageInstallerService> gDefault = new Singleton<VPackageInstallerService>() {
@Override
protected VPackageInstallerService create() {
return new VPackageInstallerService();
}
};
/**
* Used for generating session IDs. Since this is created at boot time,
* normal random might be predictable.
*/
private final Random mRandom = new SecureRandom();
private final SparseArray<PackageInstallerSession> mSessions = new SparseArray<>();
private final Handler mInstallHandler;
private final Callbacks mCallbacks;
private final HandlerThread mInstallThread;
private final InternalCallback mInternalCallback = new InternalCallback();
private Context mContext;
private VPackageInstallerService() {
mContext = VirtualCore.get().getContext();
mInstallThread = new HandlerThread("PackageInstaller");
mInstallHandler = new Handler(mInstallThread.getLooper());
mCallbacks = new Callbacks(mInstallThread.getLooper());
}
public static VPackageInstallerService get() {
return gDefault.get();
}
private static int getSessionCount(SparseArray<PackageInstallerSession> sessions,
int installerUid) {
int count = 0;
final int size = sessions.size();
for (int i = 0; i < size; i++) {
final PackageInstallerSession session = sessions.valueAt(i);
if (session.installerUid == installerUid) {
count++;
}
}
return count;
}
@Override
public int createSession(SessionParams params, String installerPackageName, int userId) throws RemoteException {
try {
return createSessionInternal(params, installerPackageName, userId);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
private int createSessionInternal(SessionParams params, String installerPackageName, int userId)
throws IOException {
final int callingUid = VBinder.getCallingUid();
final int sessionId;
final PackageInstallerSession session;
synchronized (mSessions) {
// Sanity check that installer isn't going crazy
final int activeCount = getSessionCount(mSessions, callingUid);
if (activeCount >= MAX_ACTIVE_SESSIONS) {
throw new IllegalStateException(
"Too many active sessions for UID " + callingUid);
}
sessionId = allocateSessionIdLocked();
session = new PackageInstallerSession(mInternalCallback, mContext, mInstallHandler.getLooper(), installerPackageName, sessionId, userId, callingUid, params, VEnvironment.getPackageInstallerStageDir());
}
mCallbacks.notifySessionCreated(session.sessionId, session.userId);
return sessionId;
}
@Override
public void updateSessionAppIcon(int sessionId, Bitmap appIcon) {
synchronized (mSessions) {
final PackageInstallerSession session = mSessions.get(sessionId);
if (session == null || !isCallingUidOwner(session)) {
throw new SecurityException("Caller has no access to session " + sessionId);
}
session.params.appIcon = appIcon;
session.params.appIconLastModified = -1;
mInternalCallback.onSessionBadgingChanged(session);
}
}
@Override
public void updateSessionAppLabel(int sessionId, String appLabel) throws RemoteException {
synchronized (mSessions) {
final PackageInstallerSession session = mSessions.get(sessionId);
if (session == null || !isCallingUidOwner(session)) {
throw new SecurityException("Caller has no access to session " + sessionId);
}
session.params.appLabel = appLabel;
mInternalCallback.onSessionBadgingChanged(session);
}
}
@Override
public void abandonSession(int sessionId) throws RemoteException {
synchronized (mSessions) {
final PackageInstallerSession session = mSessions.get(sessionId);
if (session == null || !isCallingUidOwner(session)) {
throw new SecurityException("Caller has no access to session " + sessionId);
}
session.abandon();
}
}
@Override
public IPackageInstallerSession openSession(int sessionId) throws RemoteException {
try {
return openSessionInternal(sessionId);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
private IPackageInstallerSession openSessionInternal(int sessionId) throws IOException {
synchronized (mSessions) {
final PackageInstallerSession session = mSessions.get(sessionId);
if (session == null || !isCallingUidOwner(session)) {
throw new SecurityException("Caller has no access to session " + sessionId);
}
session.open();
return session;
}
}
@Override
public SessionInfo getSessionInfo(int sessionId) throws RemoteException {
synchronized (mSessions) {
final PackageInstallerSession session = mSessions.get(sessionId);
return session != null ? session.generateInfo() : null;
}
}
@Override
public VParceledListSlice getAllSessions(int userId) throws RemoteException {
final List<SessionInfo> result = new ArrayList<>();
synchronized (mSessions) {
for (int i = 0; i < mSessions.size(); i++) {
final PackageInstallerSession session = mSessions.valueAt(i);
if (session.userId == userId) {
result.add(session.generateInfo());
}
}
}
return new VParceledListSlice<>(result);
}
@Override
public VParceledListSlice getMySessions(String installerPackageName, int userId) throws RemoteException {
final List<SessionInfo> result = new ArrayList<>();
synchronized (mSessions) {
for (int i = 0; i < mSessions.size(); i++) {
final PackageInstallerSession session = mSessions.valueAt(i);
if (ObjectsCompat.equals(session.installerPackageName, installerPackageName)
&& session.userId == userId) {
result.add(session.generateInfo());
}
}
}
return new VParceledListSlice<>(result);
}
@Override
public void registerCallback(IPackageInstallerCallback callback, int userId) throws RemoteException {
mCallbacks.register(callback, userId);
}
@Override
public void unregisterCallback(IPackageInstallerCallback callback) throws RemoteException {
mCallbacks.unregister(callback);
}
@Override
public void uninstall(String packageName, String callerPackageName, int flags, IntentSender statusReceiver, int userId) throws RemoteException {
boolean success = VAppManagerService.get().uninstallPackage(packageName);
if (statusReceiver != null) {
final Intent fillIn = new Intent();
fillIn.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName);
fillIn.putExtra(PackageInstaller.EXTRA_STATUS, success ? PackageInstaller.STATUS_SUCCESS : PackageInstaller.STATUS_FAILURE);
fillIn.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, PackageHelper.deleteStatusToString(success));
fillIn.putExtra("android.content.pm.extra.LEGACY_STATUS", success ? 1 : -1);
try {
statusReceiver.sendIntent(mContext, 0, fillIn, null, null);
} catch (IntentSender.SendIntentException e) {
e.printStackTrace();
}
}
}
@Override
public void setPermissionsResult(int sessionId, boolean accepted) throws RemoteException {
synchronized (mSessions) {
PackageInstallerSession session = mSessions.get(sessionId);
if (session != null) {
session.setPermissionsResult(accepted);
}
}
}
private boolean isCallingUidOwner(PackageInstallerSession session) {
return true;
}
private int allocateSessionIdLocked() {
int n = 0;
int sessionId;
do {
sessionId = mRandom.nextInt(Integer.MAX_VALUE - 1) + 1;
if (mSessions.get(sessionId) == null) {
return sessionId;
}
} while (n++ < 32);
throw new IllegalStateException("Failed to allocate session ID");
}
private static class Callbacks extends Handler {
private static final int MSG_SESSION_CREATED = 1;
private static final int MSG_SESSION_BADGING_CHANGED = 2;
private static final int MSG_SESSION_ACTIVE_CHANGED = 3;
private static final int MSG_SESSION_PROGRESS_CHANGED = 4;
private static final int MSG_SESSION_FINISHED = 5;
private final RemoteCallbackList<IPackageInstallerCallback>
mCallbacks = new RemoteCallbackList<>();
public Callbacks(Looper looper) {
super(looper);
}
public void register(IPackageInstallerCallback callback, int userId) {
mCallbacks.register(callback, new VUserHandle(userId));
}
public void unregister(IPackageInstallerCallback callback) {
mCallbacks.unregister(callback);
}
@Override
public void handleMessage(Message msg) {
final int userId = msg.arg2;
final int n = mCallbacks.beginBroadcast();
for (int i = 0; i < n; i++) {
final IPackageInstallerCallback callback = mCallbacks.getBroadcastItem(i);
final VUserHandle user = (VUserHandle) mCallbacks.getBroadcastCookie(i);
// TODO: dispatch notifications for slave profiles
if (userId == user.getIdentifier()) {
try {
invokeCallback(callback, msg);
} catch (RemoteException ignored) {
}
}
}
mCallbacks.finishBroadcast();
}
private void invokeCallback(IPackageInstallerCallback callback, Message msg)
throws RemoteException {
final int sessionId = msg.arg1;
switch (msg.what) {
case MSG_SESSION_CREATED:
callback.onSessionCreated(sessionId);
break;
case MSG_SESSION_BADGING_CHANGED:
callback.onSessionBadgingChanged(sessionId);
break;
case MSG_SESSION_ACTIVE_CHANGED:
callback.onSessionActiveChanged(sessionId, (boolean) msg.obj);
break;
case MSG_SESSION_PROGRESS_CHANGED:
callback.onSessionProgressChanged(sessionId, (float) msg.obj);
break;
case MSG_SESSION_FINISHED:
callback.onSessionFinished(sessionId, (boolean) msg.obj);
break;
}
}
private void notifySessionCreated(int sessionId, int userId) {
obtainMessage(MSG_SESSION_CREATED, sessionId, userId).sendToTarget();
}
private void notifySessionBadgingChanged(int sessionId, int userId) {
obtainMessage(MSG_SESSION_BADGING_CHANGED, sessionId, userId).sendToTarget();
}
private void notifySessionActiveChanged(int sessionId, int userId, boolean active) {
obtainMessage(MSG_SESSION_ACTIVE_CHANGED, sessionId, userId, active).sendToTarget();
}
private void notifySessionProgressChanged(int sessionId, int userId, float progress) {
obtainMessage(MSG_SESSION_PROGRESS_CHANGED, sessionId, userId, progress).sendToTarget();
}
public void notifySessionFinished(int sessionId, int userId, boolean success) {
obtainMessage(MSG_SESSION_FINISHED, sessionId, userId, success).sendToTarget();
}
}
static class PackageInstallObserverAdapter extends PackageInstallObserver {
private final Context mContext;
private final IntentSender mTarget;
private final int mSessionId;
private final int mUserId;
PackageInstallObserverAdapter(Context context, IntentSender target, int sessionId, int userId) {
mContext = context;
mTarget = target;
mSessionId = sessionId;
mUserId = userId;
}
@Override
public void onUserActionRequired(Intent intent) {
final Intent fillIn = new Intent();
fillIn.putExtra(PackageInstaller.EXTRA_SESSION_ID, mSessionId);
fillIn.putExtra(PackageInstaller.EXTRA_STATUS,
PackageInstaller.STATUS_PENDING_USER_ACTION);
fillIn.putExtra(Intent.EXTRA_INTENT, intent);
try {
mTarget.sendIntent(mContext, 0, fillIn, null, null);
} catch (IntentSender.SendIntentException ignored) {
}
}
@Override
public void onPackageInstalled(String basePackageName, int returnCode, String msg,
Bundle extras) {
final Intent fillIn = new Intent();
fillIn.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, basePackageName);
fillIn.putExtra(PackageInstaller.EXTRA_SESSION_ID, mSessionId);
fillIn.putExtra(PackageInstaller.EXTRA_STATUS,
installStatusToPublicStatus(returnCode));
fillIn.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE,
installStatusToString(returnCode, msg));
fillIn.putExtra("android.content.pm.extra.LEGACY_STATUS", returnCode);
if (extras != null) {
final String existing = extras.getString("android.content.pm.extra.FAILURE_EXISTING_PACKAGE");
if (!TextUtils.isEmpty(existing)) {
fillIn.putExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME, existing);
}
}
try {
mTarget.sendIntent(mContext, 0, fillIn, null, null);
} catch (IntentSender.SendIntentException ignored) {
}
}
}
class InternalCallback {
public void onSessionBadgingChanged(PackageInstallerSession session) {
mCallbacks.notifySessionBadgingChanged(session.sessionId, session.userId);
}
public void onSessionActiveChanged(PackageInstallerSession session, boolean active) {
mCallbacks.notifySessionActiveChanged(session.sessionId, session.userId, active);
}
public void onSessionProgressChanged(PackageInstallerSession session, float progress) {
mCallbacks.notifySessionProgressChanged(session.sessionId, session.userId, progress);
}
public void onSessionFinished(final PackageInstallerSession session, boolean success) {
mCallbacks.notifySessionFinished(session.sessionId, session.userId, success);
mInstallHandler.post(new Runnable() {
@Override
public void run() {
synchronized (mSessions) {
mSessions.remove(session.sessionId);
}
}
});
}
public void onSessionPrepared(PackageInstallerSession session) {
// We prepared the destination to write into; we want to persist
// this, but it's not critical enough to block for.
}
public void onSessionSealedBlocking(PackageInstallerSession session) {
// It's very important that we block until we've recorded the
// session as being sealed, since we never want to allow mutation
// after sealing.
}
}
}