/* * Copyright (C) 2014 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 com.android.server.pm; import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; import static org.xmlpull.v1.XmlPullParser.START_TAG; import android.Manifest; import android.app.ActivityManager; import android.app.AppGlobals; import android.app.AppOpsManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PackageDeleteObserver; import android.app.PackageInstallObserver; import android.app.admin.DevicePolicyManager; import android.content.Context; import android.content.Intent; import android.content.IntentSender; import android.content.IntentSender.SendIntentException; import android.content.pm.IPackageInstaller; import android.content.pm.IPackageInstallerCallback; import android.content.pm.IPackageInstallerSession; import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageInstaller.SessionParams; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.pm.VersionedPackage; import android.graphics.Bitmap; import android.net.Uri; import android.os.Binder; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.SELinux; import android.os.UserHandle; import android.os.UserManager; import android.os.storage.StorageManager; import android.system.ErrnoException; import android.system.Os; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.ArraySet; import android.util.AtomicFile; import android.util.ExceptionUtils; import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.util.Xml; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.content.PackageHelper; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.internal.notification.SystemNotificationChannels; import com.android.internal.util.FastXmlSerializer; import com.android.internal.util.ImageUtils; import com.android.internal.util.IndentingPrintWriter; import com.android.server.IoThread; import com.android.server.LocalServices; import com.android.server.pm.permission.PermissionManagerInternal; import libcore.io.IoUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import java.io.CharArrayWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Random; public class PackageInstallerService extends IPackageInstaller.Stub { private static final String TAG = "PackageInstaller"; private static final boolean LOGD = false; // TODO: remove outstanding sessions when installer package goes away // TODO: notify listeners in other users when package has been installed there // TODO: purge expired sessions periodically in addition to at reboot /** XML constants used in {@link #mSessionsFile} */ private static final String TAG_SESSIONS = "sessions"; /** Automatically destroy sessions older than this */ private static final long MAX_AGE_MILLIS = 3 * DateUtils.DAY_IN_MILLIS; /** Upper bound on number of active sessions for a UID */ private static final long MAX_ACTIVE_SESSIONS = 1024; /** Upper bound on number of historical sessions for a UID */ private static final long MAX_HISTORICAL_SESSIONS = 1048576; private final Context mContext; private final PackageManagerService mPm; private final PermissionManagerInternal mPermissionManager; private AppOpsManager mAppOps; private final HandlerThread mInstallThread; private final Handler mInstallHandler; private final Callbacks mCallbacks; /** * File storing persisted {@link #mSessions} metadata. */ private final AtomicFile mSessionsFile; /** * Directory storing persisted {@link #mSessions} metadata which is too * heavy to store directly in {@link #mSessionsFile}. */ private final File mSessionsDir; private final InternalCallback mInternalCallback = new InternalCallback(); /** * Used for generating session IDs. Since this is created at boot time, * normal random might be predictable. */ private final Random mRandom = new SecureRandom(); /** All sessions allocated */ @GuardedBy("mSessions") private final SparseBooleanArray mAllocatedSessions = new SparseBooleanArray(); @GuardedBy("mSessions") private final SparseArray mSessions = new SparseArray<>(); /** Historical sessions kept around for debugging purposes */ @GuardedBy("mSessions") private final List mHistoricalSessions = new ArrayList<>(); @GuardedBy("mSessions") private final SparseIntArray mHistoricalSessionsByInstaller = new SparseIntArray(); /** Sessions allocated to legacy users */ @GuardedBy("mSessions") private final SparseBooleanArray mLegacySessions = new SparseBooleanArray(); private static final FilenameFilter sStageFilter = new FilenameFilter() { @Override public boolean accept(File dir, String name) { return isStageName(name); } }; public PackageInstallerService(Context context, PackageManagerService pm) { mContext = context; mPm = pm; mPermissionManager = LocalServices.getService(PermissionManagerInternal.class); mInstallThread = new HandlerThread(TAG); mInstallThread.start(); mInstallHandler = new Handler(mInstallThread.getLooper()); mCallbacks = new Callbacks(mInstallThread.getLooper()); mSessionsFile = new AtomicFile( new File(Environment.getDataSystemDirectory(), "install_sessions.xml")); mSessionsDir = new File(Environment.getDataSystemDirectory(), "install_sessions"); mSessionsDir.mkdirs(); } public void systemReady() { mAppOps = mContext.getSystemService(AppOpsManager.class); synchronized (mSessions) { readSessionsLocked(); reconcileStagesLocked(StorageManager.UUID_PRIVATE_INTERNAL, false /*isInstant*/); reconcileStagesLocked(StorageManager.UUID_PRIVATE_INTERNAL, true /*isInstant*/); final ArraySet unclaimedIcons = newArraySet( mSessionsDir.listFiles()); // Ignore stages and icons claimed by active sessions for (int i = 0; i < mSessions.size(); i++) { final PackageInstallerSession session = mSessions.valueAt(i); unclaimedIcons.remove(buildAppIconFile(session.sessionId)); } // Clean up orphaned icons for (File icon : unclaimedIcons) { Slog.w(TAG, "Deleting orphan icon " + icon); icon.delete(); } } } private void reconcileStagesLocked(String volumeUuid, boolean isEphemeral) { final File stagingDir = buildStagingDir(volumeUuid, isEphemeral); final ArraySet unclaimedStages = newArraySet( stagingDir.listFiles(sStageFilter)); // Ignore stages claimed by active sessions for (int i = 0; i < mSessions.size(); i++) { final PackageInstallerSession session = mSessions.valueAt(i); unclaimedStages.remove(session.stageDir); } // Clean up orphaned staging directories for (File stage : unclaimedStages) { Slog.w(TAG, "Deleting orphan stage " + stage); synchronized (mPm.mInstallLock) { mPm.removeCodePathLI(stage); } } } public void onPrivateVolumeMounted(String volumeUuid) { synchronized (mSessions) { reconcileStagesLocked(volumeUuid, false /*isInstant*/); } } public static boolean isStageName(String name) { final boolean isFile = name.startsWith("vmdl") && name.endsWith(".tmp"); final boolean isContainer = name.startsWith("smdl") && name.endsWith(".tmp"); final boolean isLegacyContainer = name.startsWith("smdl2tmp"); return isFile || isContainer || isLegacyContainer; } @Deprecated public File allocateStageDirLegacy(String volumeUuid, boolean isEphemeral) throws IOException { synchronized (mSessions) { try { final int sessionId = allocateSessionIdLocked(); mLegacySessions.put(sessionId, true); final File stageDir = buildStageDir(volumeUuid, sessionId, isEphemeral); prepareStageDir(stageDir); return stageDir; } catch (IllegalStateException e) { throw new IOException(e); } } } @Deprecated public String allocateExternalStageCidLegacy() { synchronized (mSessions) { final int sessionId = allocateSessionIdLocked(); mLegacySessions.put(sessionId, true); return "smdl" + sessionId + ".tmp"; } } private void readSessionsLocked() { if (LOGD) Slog.v(TAG, "readSessionsLocked()"); mSessions.clear(); FileInputStream fis = null; try { fis = mSessionsFile.openRead(); final XmlPullParser in = Xml.newPullParser(); in.setInput(fis, StandardCharsets.UTF_8.name()); int type; while ((type = in.next()) != END_DOCUMENT) { if (type == START_TAG) { final String tag = in.getName(); if (PackageInstallerSession.TAG_SESSION.equals(tag)) { final PackageInstallerSession session; try { session = PackageInstallerSession.readFromXml(in, mInternalCallback, mContext, mPm, mInstallThread.getLooper(), mSessionsDir); } catch (Exception e) { Slog.e(TAG, "Could not read session", e); continue; } final long age = System.currentTimeMillis() - session.createdMillis; final boolean valid; if (age >= MAX_AGE_MILLIS) { Slog.w(TAG, "Abandoning old session first created at " + session.createdMillis); valid = false; } else { valid = true; } if (valid) { mSessions.put(session.sessionId, session); } else { // Since this is early during boot we don't send // any observer events about the session, but we // keep details around for dumpsys. addHistoricalSessionLocked(session); } mAllocatedSessions.put(session.sessionId, true); } } } } catch (FileNotFoundException e) { // Missing sessions are okay, probably first boot } catch (IOException | XmlPullParserException e) { Slog.wtf(TAG, "Failed reading install sessions", e); } finally { IoUtils.closeQuietly(fis); } } private void addHistoricalSessionLocked(PackageInstallerSession session) { CharArrayWriter writer = new CharArrayWriter(); IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); session.dump(pw); mHistoricalSessions.add(writer.toString()); int installerUid = session.getInstallerUid(); // Increment the number of sessions by this installerUid. mHistoricalSessionsByInstaller.put(installerUid, mHistoricalSessionsByInstaller.get(installerUid) + 1); } private void writeSessionsLocked() { if (LOGD) Slog.v(TAG, "writeSessionsLocked()"); FileOutputStream fos = null; try { fos = mSessionsFile.startWrite(); XmlSerializer out = new FastXmlSerializer(); out.setOutput(fos, StandardCharsets.UTF_8.name()); out.startDocument(null, true); out.startTag(null, TAG_SESSIONS); final int size = mSessions.size(); for (int i = 0; i < size; i++) { final PackageInstallerSession session = mSessions.valueAt(i); session.write(out, mSessionsDir); } out.endTag(null, TAG_SESSIONS); out.endDocument(); mSessionsFile.finishWrite(fos); } catch (IOException e) { if (fos != null) { mSessionsFile.failWrite(fos); } } } private File buildAppIconFile(int sessionId) { return new File(mSessionsDir, "app_icon." + sessionId + ".png"); } private void writeSessionsAsync() { IoThread.getHandler().post(new Runnable() { @Override public void run() { synchronized (mSessions) { writeSessionsLocked(); } } }); } @Override public int createSession(SessionParams params, String installerPackageName, int userId) { try { return createSessionInternal(params, installerPackageName, userId); } catch (IOException e) { throw ExceptionUtils.wrap(e); } } private int createSessionInternal(SessionParams params, String installerPackageName, int userId) throws IOException { final int callingUid = Binder.getCallingUid(); mPermissionManager.enforceCrossUserPermission( callingUid, userId, true, true, "createSession"); if (mPm.isUserRestricted(userId, UserManager.DISALLOW_INSTALL_APPS)) { throw new SecurityException("User restriction prevents installing"); } if ((callingUid == Process.SHELL_UID) || (callingUid == Process.ROOT_UID)) { params.installFlags |= PackageManager.INSTALL_FROM_ADB; } else { mAppOps.checkPackage(callingUid, installerPackageName); params.installFlags &= ~PackageManager.INSTALL_FROM_ADB; params.installFlags &= ~PackageManager.INSTALL_ALL_USERS; params.installFlags |= PackageManager.INSTALL_REPLACE_EXISTING; if ((params.installFlags & PackageManager.INSTALL_VIRTUAL_PRELOAD) != 0 && !mPm.isCallerVerifier(callingUid)) { params.installFlags &= ~PackageManager.INSTALL_VIRTUAL_PRELOAD; } } // Only system components can circumvent runtime permissions when installing. if ((params.installFlags & PackageManager.INSTALL_GRANT_RUNTIME_PERMISSIONS) != 0 && mContext.checkCallingOrSelfPermission(Manifest.permission .INSTALL_GRANT_RUNTIME_PERMISSIONS) == PackageManager.PERMISSION_DENIED) { throw new SecurityException("You need the " + "android.permission.INSTALL_GRANT_RUNTIME_PERMISSIONS permission " + "to use the PackageManager.INSTALL_GRANT_RUNTIME_PERMISSIONS flag"); } if ((params.installFlags & PackageManager.INSTALL_FORWARD_LOCK) != 0 || (params.installFlags & PackageManager.INSTALL_EXTERNAL) != 0) { throw new IllegalArgumentException( "New installs into ASEC containers no longer supported"); } // Defensively resize giant app icons if (params.appIcon != null) { final ActivityManager am = (ActivityManager) mContext.getSystemService( Context.ACTIVITY_SERVICE); final int iconSize = am.getLauncherLargeIconSize(); if ((params.appIcon.getWidth() > iconSize * 2) || (params.appIcon.getHeight() > iconSize * 2)) { params.appIcon = Bitmap.createScaledBitmap(params.appIcon, iconSize, iconSize, true); } } switch (params.mode) { case SessionParams.MODE_FULL_INSTALL: case SessionParams.MODE_INHERIT_EXISTING: break; default: throw new IllegalArgumentException("Invalid install mode: " + params.mode); } // If caller requested explicit location, sanity check it, otherwise // resolve the best internal or adopted location. if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) { if (!PackageHelper.fitsOnInternal(mContext, params)) { throw new IOException("No suitable internal storage available"); } } else if ((params.installFlags & PackageManager.INSTALL_EXTERNAL) != 0) { if (!PackageHelper.fitsOnExternal(mContext, params)) { throw new IOException("No suitable external storage available"); } } else if ((params.installFlags & PackageManager.INSTALL_FORCE_VOLUME_UUID) != 0) { // For now, installs to adopted media are treated as internal from // an install flag point-of-view. params.setInstallFlagsInternal(); } else { // For now, installs to adopted media are treated as internal from // an install flag point-of-view. params.setInstallFlagsInternal(); // Resolve best location for install, based on combination of // requested install flags, delta size, and manifest settings. final long ident = Binder.clearCallingIdentity(); try { params.volumeUuid = PackageHelper.resolveInstallVolume(mContext, params); } finally { Binder.restoreCallingIdentity(ident); } } 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); } final int historicalCount = mHistoricalSessionsByInstaller.get(callingUid); if (historicalCount >= MAX_HISTORICAL_SESSIONS) { throw new IllegalStateException( "Too many historical sessions for UID " + callingUid); } sessionId = allocateSessionIdLocked(); } final long createdMillis = System.currentTimeMillis(); // We're staging to exactly one location File stageDir = null; String stageCid = null; if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) { final boolean isInstant = (params.installFlags & PackageManager.INSTALL_INSTANT_APP) != 0; stageDir = buildStageDir(params.volumeUuid, sessionId, isInstant); } else { stageCid = buildExternalStageCid(sessionId); } session = new PackageInstallerSession(mInternalCallback, mContext, mPm, mInstallThread.getLooper(), sessionId, userId, installerPackageName, callingUid, params, createdMillis, stageDir, stageCid, false, false); synchronized (mSessions) { mSessions.put(sessionId, session); } mCallbacks.notifySessionCreated(session.sessionId, session.userId); writeSessionsAsync(); 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); } // Defensively resize giant app icons if (appIcon != null) { final ActivityManager am = (ActivityManager) mContext.getSystemService( Context.ACTIVITY_SERVICE); final int iconSize = am.getLauncherLargeIconSize(); if ((appIcon.getWidth() > iconSize * 2) || (appIcon.getHeight() > iconSize * 2)) { appIcon = Bitmap.createScaledBitmap(appIcon, iconSize, iconSize, true); } } session.params.appIcon = appIcon; session.params.appIconLastModified = -1; mInternalCallback.onSessionBadgingChanged(session); } } @Override public void updateSessionAppLabel(int sessionId, String appLabel) { 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) { 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) { try { return openSessionInternal(sessionId); } catch (IOException e) { throw ExceptionUtils.wrap(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; } } private int allocateSessionIdLocked() { int n = 0; int sessionId; do { sessionId = mRandom.nextInt(Integer.MAX_VALUE - 1) + 1; if (!mAllocatedSessions.get(sessionId, false)) { mAllocatedSessions.put(sessionId, true); return sessionId; } } while (n++ < 32); throw new IllegalStateException("Failed to allocate session ID"); } private File buildStagingDir(String volumeUuid, boolean isEphemeral) { return Environment.getDataAppDirectory(volumeUuid); } private File buildStageDir(String volumeUuid, int sessionId, boolean isEphemeral) { final File stagingDir = buildStagingDir(volumeUuid, isEphemeral); return new File(stagingDir, "vmdl" + sessionId + ".tmp"); } static void prepareStageDir(File stageDir) throws IOException { if (stageDir.exists()) { throw new IOException("Session dir already exists: " + stageDir); } try { Os.mkdir(stageDir.getAbsolutePath(), 0755); Os.chmod(stageDir.getAbsolutePath(), 0755); } catch (ErrnoException e) { // This purposefully throws if directory already exists throw new IOException("Failed to prepare session dir: " + stageDir, e); } if (!SELinux.restorecon(stageDir)) { throw new IOException("Failed to restorecon session dir: " + stageDir); } } private String buildExternalStageCid(int sessionId) { return "smdl" + sessionId + ".tmp"; } @Override public SessionInfo getSessionInfo(int sessionId) { synchronized (mSessions) { final PackageInstallerSession session = mSessions.get(sessionId); return session != null ? session.generateInfo() : null; } } @Override public ParceledListSlice getAllSessions(int userId) { mPermissionManager.enforceCrossUserPermission( Binder.getCallingUid(), userId, true, false, "getAllSessions"); final List 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(false)); } } } return new ParceledListSlice<>(result); } @Override public ParceledListSlice getMySessions(String installerPackageName, int userId) { mPermissionManager.enforceCrossUserPermission( Binder.getCallingUid(), userId, true, false, "getMySessions"); mAppOps.checkPackage(Binder.getCallingUid(), installerPackageName); final List result = new ArrayList<>(); synchronized (mSessions) { for (int i = 0; i < mSessions.size(); i++) { final PackageInstallerSession session = mSessions.valueAt(i); SessionInfo info = session.generateInfo(false); if (Objects.equals(info.getInstallerPackageName(), installerPackageName) && session.userId == userId) { result.add(info); } } } return new ParceledListSlice<>(result); } @Override public void uninstall(VersionedPackage versionedPackage, String callerPackageName, int flags, IntentSender statusReceiver, int userId) throws RemoteException { final int callingUid = Binder.getCallingUid(); mPermissionManager.enforceCrossUserPermission(callingUid, userId, true, true, "uninstall"); if ((callingUid != Process.SHELL_UID) && (callingUid != Process.ROOT_UID)) { mAppOps.checkPackage(callingUid, callerPackageName); } // Check whether the caller is device owner, in which case we do it silently. DevicePolicyManager dpm = (DevicePolicyManager) mContext.getSystemService( Context.DEVICE_POLICY_SERVICE); boolean isDeviceOwner = (dpm != null) && dpm.isDeviceOwnerAppOnCallingUser( callerPackageName); final PackageDeleteObserverAdapter adapter = new PackageDeleteObserverAdapter(mContext, statusReceiver, versionedPackage.getPackageName(), isDeviceOwner, userId); if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DELETE_PACKAGES) == PackageManager.PERMISSION_GRANTED) { // Sweet, call straight through! mPm.deletePackageVersioned(versionedPackage, adapter.getBinder(), userId, flags); } else if (isDeviceOwner) { // Allow the DeviceOwner to silently delete packages // Need to clear the calling identity to get DELETE_PACKAGES permission long ident = Binder.clearCallingIdentity(); try { mPm.deletePackageVersioned(versionedPackage, adapter.getBinder(), userId, flags); } finally { Binder.restoreCallingIdentity(ident); } } else { // Take a short detour to confirm with user final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE); intent.setData(Uri.fromParts("package", versionedPackage.getPackageName(), null)); intent.putExtra(PackageInstaller.EXTRA_CALLBACK, adapter.getBinder().asBinder()); adapter.onUserActionRequired(intent); } } @Override public void setPermissionsResult(int sessionId, boolean accepted) { mContext.enforceCallingOrSelfPermission(android.Manifest.permission.INSTALL_PACKAGES, TAG); synchronized (mSessions) { PackageInstallerSession session = mSessions.get(sessionId); if (session != null) { session.setPermissionsResult(accepted); } } } @Override public void registerCallback(IPackageInstallerCallback callback, int userId) { mPermissionManager.enforceCrossUserPermission( Binder.getCallingUid(), userId, true, false, "registerCallback"); mCallbacks.register(callback, userId); } @Override public void unregisterCallback(IPackageInstallerCallback callback) { mCallbacks.unregister(callback); } private static int getSessionCount(SparseArray 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.getInstallerUid() == installerUid) { count++; } } return count; } private boolean isCallingUidOwner(PackageInstallerSession session) { final int callingUid = Binder.getCallingUid(); if (callingUid == Process.ROOT_UID) { return true; } else { return (session != null) && (callingUid == session.getInstallerUid()); } } static class PackageDeleteObserverAdapter extends PackageDeleteObserver { private final Context mContext; private final IntentSender mTarget; private final String mPackageName; private final Notification mNotification; public PackageDeleteObserverAdapter(Context context, IntentSender target, String packageName, boolean showNotification, int userId) { mContext = context; mTarget = target; mPackageName = packageName; if (showNotification) { mNotification = buildSuccessNotification(mContext, mContext.getResources().getString(R.string.package_deleted_device_owner), packageName, userId); } else { mNotification = null; } } @Override public void onUserActionRequired(Intent intent) { final Intent fillIn = new Intent(); fillIn.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, mPackageName); 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 (SendIntentException ignored) { } } @Override public void onPackageDeleted(String basePackageName, int returnCode, String msg) { if (PackageManager.DELETE_SUCCEEDED == returnCode && mNotification != null) { NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(basePackageName, SystemMessage.NOTE_PACKAGE_STATE, mNotification); } final Intent fillIn = new Intent(); fillIn.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, mPackageName); fillIn.putExtra(PackageInstaller.EXTRA_STATUS, PackageManager.deleteStatusToPublicStatus(returnCode)); fillIn.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, PackageManager.deleteStatusToString(returnCode, msg)); fillIn.putExtra(PackageInstaller.EXTRA_LEGACY_STATUS, returnCode); try { mTarget.sendIntent(mContext, 0, fillIn, null, null); } catch (SendIntentException ignored) { } } } static class PackageInstallObserverAdapter extends PackageInstallObserver { private final Context mContext; private final IntentSender mTarget; private final int mSessionId; private final boolean mShowNotification; private final int mUserId; public PackageInstallObserverAdapter(Context context, IntentSender target, int sessionId, boolean showNotification, int userId) { mContext = context; mTarget = target; mSessionId = sessionId; mShowNotification = showNotification; 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 (SendIntentException ignored) { } } @Override public void onPackageInstalled(String basePackageName, int returnCode, String msg, Bundle extras) { if (PackageManager.INSTALL_SUCCEEDED == returnCode && mShowNotification) { boolean update = (extras != null) && extras.getBoolean(Intent.EXTRA_REPLACING); Notification notification = buildSuccessNotification(mContext, mContext.getResources() .getString(update ? R.string.package_updated_device_owner : R.string.package_installed_device_owner), basePackageName, mUserId); if (notification != null) { NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(basePackageName, SystemMessage.NOTE_PACKAGE_STATE, notification); } } final Intent fillIn = new Intent(); fillIn.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, basePackageName); fillIn.putExtra(PackageInstaller.EXTRA_SESSION_ID, mSessionId); fillIn.putExtra(PackageInstaller.EXTRA_STATUS, PackageManager.installStatusToPublicStatus(returnCode)); fillIn.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, PackageManager.installStatusToString(returnCode, msg)); fillIn.putExtra(PackageInstaller.EXTRA_LEGACY_STATUS, returnCode); if (extras != null) { final String existing = extras.getString( PackageManager.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 (SendIntentException ignored) { } } } /** * Build a notification for package installation / deletion by device owners that is shown if * the operation succeeds. */ private static Notification buildSuccessNotification(Context context, String contentText, String basePackageName, int userId) { PackageInfo packageInfo = null; try { packageInfo = AppGlobals.getPackageManager().getPackageInfo( basePackageName, PackageManager.MATCH_STATIC_SHARED_LIBRARIES, userId); } catch (RemoteException ignored) { } if (packageInfo == null || packageInfo.applicationInfo == null) { Slog.w(TAG, "Notification not built for package: " + basePackageName); return null; } PackageManager pm = context.getPackageManager(); Bitmap packageIcon = ImageUtils.buildScaledBitmap( packageInfo.applicationInfo.loadIcon(pm), context.getResources().getDimensionPixelSize( android.R.dimen.notification_large_icon_width), context.getResources().getDimensionPixelSize( android.R.dimen.notification_large_icon_height)); CharSequence packageLabel = packageInfo.applicationInfo.loadLabel(pm); return new Notification.Builder(context, SystemNotificationChannels.DEVICE_ADMIN) .setSmallIcon(R.drawable.ic_check_circle_24px) .setColor(context.getResources().getColor( R.color.system_notification_accent_color)) .setContentTitle(packageLabel) .setContentText(contentText) .setStyle(new Notification.BigTextStyle().bigText(contentText)) .setLargeIcon(packageIcon) .build(); } public static ArraySet newArraySet(E... elements) { final ArraySet set = new ArraySet(); if (elements != null) { set.ensureCapacity(elements.length); Collections.addAll(set, elements); } return set; } 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 mCallbacks = new RemoteCallbackList<>(); public Callbacks(Looper looper) { super(looper); } public void register(IPackageInstallerCallback callback, int userId) { mCallbacks.register(callback, new UserHandle(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 UserHandle user = (UserHandle) 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(); } } void dump(IndentingPrintWriter pw) { synchronized (mSessions) { pw.println("Active install sessions:"); pw.increaseIndent(); int N = mSessions.size(); for (int i = 0; i < N; i++) { final PackageInstallerSession session = mSessions.valueAt(i); session.dump(pw); pw.println(); } pw.println(); pw.decreaseIndent(); pw.println("Historical install sessions:"); pw.increaseIndent(); N = mHistoricalSessions.size(); for (int i = 0; i < N; i++) { pw.print(mHistoricalSessions.get(i)); pw.println(); } pw.println(); pw.decreaseIndent(); pw.println("Legacy install sessions:"); pw.increaseIndent(); pw.println(mLegacySessions.toString()); pw.decreaseIndent(); } } class InternalCallback { public void onSessionBadgingChanged(PackageInstallerSession session) { mCallbacks.notifySessionBadgingChanged(session.sessionId, session.userId); writeSessionsAsync(); } 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); addHistoricalSessionLocked(session); final File appIconFile = buildAppIconFile(session.sessionId); if (appIconFile.exists()) { appIconFile.delete(); } writeSessionsLocked(); } } }); } 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. writeSessionsAsync(); } 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. synchronized (mSessions) { writeSessionsLocked(); } } } }