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.IPackageInstallObserver2; import android.content.pm.IPackageInstallerSession; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import android.text.TextUtils; import com.lody.virtual.helper.utils.FileUtils; import com.lody.virtual.helper.utils.VLog; import java.io.File; import java.io.FileDescriptor; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import static android.system.OsConstants.O_CREAT; import static android.system.OsConstants.O_RDONLY; import static android.system.OsConstants.O_WRONLY; /** * @author Lody */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public class PackageInstallerSession extends IPackageInstallerSession.Stub { public static final int INSTALL_FAILED_INTERNAL_ERROR = -110; public static final int INSTALL_FAILED_ABORTED = -115; public static final int INSTALL_SUCCEEDED = 1; public static final int INSTALL_FAILED_INVALID_APK = -2; private static final String TAG = "PackageInstaller"; private static final String REMOVE_SPLIT_MARKER_EXTENSION = ".removed"; private static final int MSG_COMMIT = 0; private final VPackageInstallerService.InternalCallback mCallback; private final Context mContext; private final Handler mHandler; final int sessionId; final int userId; final int installerUid; final SessionParams params; final String installerPackageName; private boolean mPermissionsAccepted; /** * Staging location where client data is written. */ final File stageDir; private final AtomicInteger mActiveCount = new AtomicInteger(); private final Object mLock = new Object(); private float mClientProgress = 0; private float mInternalProgress = 0; private float mProgress = 0; private float mReportedProgress = -1; private boolean mPrepared = false; private boolean mSealed = false; private boolean mDestroyed = false; private int mFinalStatus; private String mFinalMessage; private IPackageInstallObserver2 mRemoteObserver; private ArrayList<FileBridge> mBridges = new ArrayList<>(); private File mResolvedStageDir; /** * Fields derived from commit parsing */ private String mPackageName; private File mResolvedBaseFile; private final List<File> mResolvedStagedFiles = new ArrayList<>(); private final Handler.Callback mHandlerCallback = new Handler.Callback() { @Override public boolean handleMessage(Message msg) { synchronized (mLock) { if (msg.obj != null) { mRemoteObserver = (IPackageInstallObserver2) msg.obj; } try { commitLocked(); } catch (PackageManagerException e) { final String completeMsg = getCompleteMessage(e); VLog.e(TAG, "Commit of session " + sessionId + " failed: " + completeMsg); destroyInternal(); dispatchSessionFinished(e.error, completeMsg, null); } return true; } } }; public PackageInstallerSession(VPackageInstallerService.InternalCallback callback, Context context, Looper looper, String installerPackageName, int sessionId, int userId, int installerUid, SessionParams params, File stageDir) { this.mCallback = callback; this.mContext = context; this.mHandler = new Handler(looper, mHandlerCallback); this.installerPackageName = installerPackageName; this.sessionId = sessionId; this.userId = userId; this.installerUid = installerUid; this.mPackageName = params.appPackageName; this.params = params; this.stageDir = stageDir; } public SessionInfo generateInfo() { final SessionInfo info = new SessionInfo(); synchronized (mLock) { info.sessionId = sessionId; info.installerPackageName = installerPackageName; info.resolvedBaseCodePath = (mResolvedBaseFile != null) ? mResolvedBaseFile.getAbsolutePath() : null; info.progress = mProgress; info.sealed = mSealed; info.active = mActiveCount.get() > 0; info.mode = params.mode; info.sizeBytes = params.sizeBytes; info.appPackageName = params.appPackageName; info.appIcon = params.appIcon; info.appLabel = params.appLabel; } return info; } private void commitLocked() throws PackageManagerException { if (mDestroyed) { throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, "Session destroyed"); } if (!mSealed) { throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, "Session not sealed"); } try { resolveStageDir(); } catch (IOException e) { e.printStackTrace(); } validateInstallLocked(); mInternalProgress = 0.5f; computeProgressLocked(true); // We've reached point of no return; call into PMS to install the stage. // Regardless of success or failure we always destroy session. final IPackageInstallObserver2 localObserver = new IPackageInstallObserver2.Stub() { @Override public void onUserActionRequired(Intent intent) { throw new IllegalStateException(); } @Override public void onPackageInstalled(String basePackageName, int returnCode, String msg, Bundle extras) { destroyInternal(); dispatchSessionFinished(returnCode, msg, extras); } }; } private void validateInstallLocked() throws PackageManagerException { mResolvedBaseFile = null; mResolvedStagedFiles.clear(); File[] addedFiles = this.mResolvedStageDir.listFiles(); if (addedFiles == null || addedFiles.length == 0) { throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, "No packages staged"); } for (File addedFile : addedFiles) { if (!addedFile.isDirectory()) { final String targetName = "base.apk"; final File targetFile = new File(mResolvedStageDir, targetName); if (!addedFile.equals(targetFile)) { addedFile.renameTo(targetFile); } mResolvedBaseFile = targetFile; mResolvedStagedFiles.add(targetFile); } } if (mResolvedBaseFile == null) { throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, "Full install must include a base package"); } } @Override public void setClientProgress(float progress) throws RemoteException { synchronized (mLock) { // Always publish first staging movement final boolean forcePublish = (mClientProgress == 0); mClientProgress = progress; computeProgressLocked(forcePublish); } } private static float constrain(float amount, float low, float high) { return amount < low ? low : (amount > high ? high : amount); } private void computeProgressLocked(boolean forcePublish) { mProgress = constrain(mClientProgress * 0.8f, 0f, 0.8f) + constrain(mInternalProgress * 0.2f, 0f, 0.2f); // Only publish when meaningful change if (forcePublish || Math.abs(mProgress - mReportedProgress) >= 0.01) { mReportedProgress = mProgress; mCallback.onSessionProgressChanged(this, mProgress); } } @Override public void addClientProgress(float progress) throws RemoteException { synchronized (mLock) { setClientProgress(mClientProgress + progress); } } @Override public String[] getNames() throws RemoteException { assertPreparedAndNotSealed("getNames"); try { return resolveStageDir().list(); } catch (IOException e) { throw new IllegalStateException(e); } } /** * Resolve the actual location where staged data should be written. This * might point at an ASEC mount point, which is why we delay path resolution * until someone actively works with the session. */ private File resolveStageDir() throws IOException { synchronized (mLock) { if (mResolvedStageDir == null && stageDir != null) { mResolvedStageDir = stageDir; if (!stageDir.exists()) { stageDir.mkdirs(); } } return mResolvedStageDir; } } @Override public ParcelFileDescriptor openWrite(String name, long offsetBytes, long lengthBytes) throws RemoteException { try { return openWriteInternal(name, offsetBytes, lengthBytes); } catch (IOException e) { throw new IllegalStateException(e); } } private void assertPreparedAndNotSealed(String cookie) { synchronized (mLock) { if (!mPrepared) { throw new IllegalStateException(cookie + " before prepared"); } if (mSealed) { throw new SecurityException(cookie + " not allowed after commit"); } } } private ParcelFileDescriptor openWriteInternal(String name, long offsetBytes, long lengthBytes) throws IOException { // Quick sanity check of state, and allocate a pipe for ourselves. We // then do heavy disk allocation outside the lock, but this open pipe // will block any attempted install transitions. final FileBridge bridge; synchronized (mLock) { assertPreparedAndNotSealed("openWrite"); bridge = new FileBridge(); mBridges.add(bridge); } try { final File target = new File(resolveStageDir(), name); // TODO: this should delegate to DCS so the system process avoids // holding open FDs into containers. final FileDescriptor targetFd = Os.open(target.getAbsolutePath(), O_CREAT | O_WRONLY, 0644); // If caller specified a total length, allocate it for them. Free up // cache space to grow, if needed. if (lengthBytes > 0) { Os.posix_fallocate(targetFd, 0, lengthBytes); } if (offsetBytes > 0) { Os.lseek(targetFd, offsetBytes, OsConstants.SEEK_SET); } bridge.setTargetFile(targetFd); bridge.start(); return ParcelFileDescriptor.dup(bridge.getClientSocket()); } catch (ErrnoException e) { throw new IOException(e); } } @Override public ParcelFileDescriptor openRead(String name) throws RemoteException { try { return openReadInternal(name); } catch (IOException e) { throw new IllegalStateException(e); } } private ParcelFileDescriptor openReadInternal(String name) throws IOException { assertPreparedAndNotSealed("openRead"); try { if (!FileUtils.isValidExtFilename(name)) { throw new IllegalArgumentException("Invalid name: " + name); } final File target = new File(resolveStageDir(), name); final FileDescriptor targetFd = Os.open(target.getAbsolutePath(), O_RDONLY, 0); return ParcelFileDescriptor.dup(targetFd); } catch (ErrnoException e) { throw new IOException(e); } } @Override public void removeSplit(String splitName) throws RemoteException { if (TextUtils.isEmpty(params.appPackageName)) { throw new IllegalStateException("Must specify package name to remove a split"); } try { createRemoveSplitMarker(splitName); } catch (IOException e) { throw new IllegalStateException(e); } } private void createRemoveSplitMarker(String splitName) throws IOException { try { final String markerName = splitName + REMOVE_SPLIT_MARKER_EXTENSION; if (!FileUtils.isValidExtFilename(markerName)) { throw new IllegalArgumentException("Invalid marker: " + markerName); } final File target = new File(resolveStageDir(), markerName); target.createNewFile(); Os.chmod(target.getAbsolutePath(), 0 /*mode*/); } catch (ErrnoException e) { throw new IOException(e); } } @Override public void close() throws RemoteException { if (mActiveCount.decrementAndGet() == 0) { mCallback.onSessionActiveChanged(this, false); } } @Override public void commit(IntentSender statusReceiver) throws RemoteException { final boolean wasSealed; synchronized (mLock) { wasSealed = mSealed; if (!mSealed) { // Verify that all writers are hands-off for (FileBridge bridge : mBridges) { if (!bridge.isClosed()) { throw new SecurityException("Files still open"); } } mSealed = true; } // Client staging is fully done at this point mClientProgress = 1f; computeProgressLocked(true); } if (!wasSealed) { // Persist the fact that we've sealed ourselves to prevent // mutations of any hard links we create. We do this without holding // the session lock, since otherwise it's a lock inversion. mCallback.onSessionSealedBlocking(this); } // This ongoing commit should keep session active, even though client // will probably close their end. mActiveCount.incrementAndGet(); final VPackageInstallerService.PackageInstallObserverAdapter adapter = new VPackageInstallerService.PackageInstallObserverAdapter(mContext, statusReceiver, sessionId, userId); mHandler.obtainMessage(MSG_COMMIT, adapter.getBinder()).sendToTarget(); } @Override public void abandon() throws RemoteException { destroyInternal(); dispatchSessionFinished(INSTALL_FAILED_ABORTED, "Session was abandoned", null); } private void destroyInternal() { synchronized (mLock) { mSealed = true; mDestroyed = true; // Force shut down all bridges for (FileBridge bridge : mBridges) { bridge.forceClose(); } } if (stageDir != null) { FileUtils.deleteDir(stageDir.getAbsolutePath()); } } private void dispatchSessionFinished(int returnCode, String msg, Bundle extras) { mFinalStatus = returnCode; mFinalMessage = msg; if (mRemoteObserver != null) { try { mRemoteObserver.onPackageInstalled(mPackageName, returnCode, msg, extras); } catch (RemoteException ignored) { } } final boolean success = (returnCode == INSTALL_SUCCEEDED); mCallback.onSessionFinished(this, success); } void setPermissionsResult(boolean accepted) { if (!mSealed) { throw new SecurityException("Must be sealed to accept permissions"); } if (accepted) { // Mark and kick off another install pass synchronized (mLock) { mPermissionsAccepted = true; } mHandler.obtainMessage(MSG_COMMIT).sendToTarget(); } else { destroyInternal(); dispatchSessionFinished(INSTALL_FAILED_ABORTED, "User rejected permissions", null); } } public void open() throws IOException { if (mActiveCount.getAndIncrement() == 0) { mCallback.onSessionActiveChanged(this, true); } synchronized (mLock) { if (!mPrepared) { if (stageDir == null) { throw new IllegalArgumentException( "Exactly one of stageDir or stageCid stage must be set"); } mPrepared = true; mCallback.onSessionPrepared(this); } } } public static String getCompleteMessage(Throwable t) { final StringBuilder builder = new StringBuilder(); builder.append(t.getMessage()); while ((t = t.getCause()) != null) { builder.append(": ").append(t.getMessage()); } return builder.toString(); } private class PackageManagerException extends Exception { public final int error; PackageManagerException(int error, String detailMessage) { super(detailMessage); this.error = error; } } }