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. } } }