/* * 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.launcher3.pm; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.LauncherApps; import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageManager; import android.os.Process; import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.WorkerThread; import com.android.launcher3.LauncherPrefs; import com.android.launcher3.LauncherSettings; import com.android.launcher3.SessionCommitReceiver; import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.logging.FileLog; import com.android.launcher3.model.ItemInstallQueue; import com.android.launcher3.testing.shared.TestProtocol; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSet; import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.Preconditions; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Objects; /** * Utility class to tracking install sessions */ public class InstallSessionHelper { @NonNull private static final String LOG = "InstallSessionHelper"; // Set of session ids of promise icons that have been added to the home screen // as FLAG_PROMISE_NEW_INSTALLS. @NonNull public static final String PROMISE_ICON_IDS = "promise_icon_ids"; private static final boolean DEBUG = false; @NonNull public static final MainThreadInitializedObject INSTANCE = new MainThreadInitializedObject<>(InstallSessionHelper::new); @Nullable private final LauncherApps mLauncherApps; @NonNull private final Context mAppContext; @NonNull private final PackageInstaller mInstaller; @NonNull private final HashMap mSessionVerifiedMap = new HashMap<>(); @Nullable private IntSet mPromiseIconIds; public InstallSessionHelper(@NonNull final Context context) { mInstaller = context.getPackageManager().getPackageInstaller(); mAppContext = context.getApplicationContext(); mLauncherApps = context.getSystemService(LauncherApps.class); } @WorkerThread @NonNull private IntSet getPromiseIconIds() { Preconditions.assertWorkerThread(); if (mPromiseIconIds != null) { return mPromiseIconIds; } mPromiseIconIds = IntSet.wrap(IntArray.fromConcatString( LauncherPrefs.get(mAppContext).get(LauncherPrefs.PROMISE_ICON_IDS))); IntArray existingIds = new IntArray(); for (SessionInfo info : getActiveSessions().values()) { existingIds.add(info.getSessionId()); } IntArray idsToRemove = new IntArray(); for (int i = mPromiseIconIds.size() - 1; i >= 0; --i) { if (!existingIds.contains(mPromiseIconIds.getArray().get(i))) { idsToRemove.add(mPromiseIconIds.getArray().get(i)); } } for (int i = idsToRemove.size() - 1; i >= 0; --i) { mPromiseIconIds.getArray().removeValue(idsToRemove.get(i)); } return mPromiseIconIds; } @NonNull public HashMap getActiveSessions() { HashMap activePackages = new HashMap<>(); for (SessionInfo info : getAllVerifiedSessions()) { activePackages.put(new PackageUserKey(info.getAppPackageName(), getUserHandle(info)), info); } return activePackages; } @Nullable public SessionInfo getActiveSessionInfo(UserHandle user, String pkg) { for (SessionInfo info : getAllVerifiedSessions()) { boolean match = pkg.equals(info.getAppPackageName()); if (Utilities.ATLEAST_Q && !user.equals(getUserHandle(info))) { match = false; } if (match) { return info; } } return null; } private void updatePromiseIconPrefs() { LauncherPrefs.get(mAppContext).put(LauncherPrefs.PROMISE_ICON_IDS, getPromiseIconIds().getArray().toConcatString()); } @Nullable SessionInfo getVerifiedSessionInfo(final int sessionId) { return verify(mInstaller.getSessionInfo(sessionId)); } @Nullable private SessionInfo verify(@Nullable final SessionInfo sessionInfo) { if (sessionInfo == null || sessionInfo.getInstallerPackageName() == null || TextUtils.isEmpty(sessionInfo.getAppPackageName())) { if (TestProtocol.sDebugTracing) { Log.d(TestProtocol.MISSING_PROMISE_ICON, LOG + " verify" + ", info=" + (sessionInfo == null) + ", info install name" + (sessionInfo == null ? null : sessionInfo.getInstallerPackageName()) + ", empty pkg name" + TextUtils.isEmpty((sessionInfo == null ? null : sessionInfo.getAppPackageName()))); } return null; } return isTrustedPackage(sessionInfo.getInstallerPackageName(), getUserHandle(sessionInfo)) ? sessionInfo : null; } /** * Returns true if the provided packageName can be trusted for user configurations */ public boolean isTrustedPackage(String pkg, UserHandle user) { synchronized (mSessionVerifiedMap) { if (!mSessionVerifiedMap.containsKey(pkg)) { boolean hasSystemFlag = new PackageManagerHelper(mAppContext).getApplicationInfo( pkg, user, ApplicationInfo.FLAG_SYSTEM) != null; mSessionVerifiedMap.put(pkg, DEBUG || hasSystemFlag); } } return mSessionVerifiedMap.get(pkg); } @NonNull public List getAllVerifiedSessions() { List list = new ArrayList<>(Utilities.ATLEAST_Q ? Objects.requireNonNull(mLauncherApps).getAllPackageInstallerSessions() : mInstaller.getAllSessions()); Iterator it = list.iterator(); while (it.hasNext()) { if (verify(it.next()) == null) { it.remove(); } } return list; } /** * Attempt to restore workspace layout if the session is triggered due to device restore. */ public boolean restoreDbIfApplicable(@NonNull final SessionInfo info) { if (!FeatureFlags.ENABLE_DATABASE_RESTORE.get()) { return false; } if (isRestore(info)) { LauncherSettings.Settings.call(mAppContext.getContentResolver(), LauncherSettings.Settings.METHOD_RESTORE_BACKUP_TABLE); return true; } return false; } @RequiresApi(26) private static boolean isRestore(@NonNull final SessionInfo info) { return info.getInstallReason() == PackageManager.INSTALL_REASON_DEVICE_RESTORE; } @WorkerThread public boolean promiseIconAddedForId(final int sessionId) { return getPromiseIconIds().contains(sessionId); } @WorkerThread public void removePromiseIconId(final int sessionId) { if (promiseIconAddedForId(sessionId)) { getPromiseIconIds().getArray().removeValue(sessionId); updatePromiseIconPrefs(); } } /** * Add a promise app icon to the workspace iff: * - The settings for it are enabled * - The user installed the app * - There is an app icon and label (For apps with no launching activity, no icon is provided). * - The app is not already installed * - A promise icon for the session has not already been created */ @WorkerThread void tryQueuePromiseAppIcon(@Nullable final PackageInstaller.SessionInfo sessionInfo) { if (TestProtocol.sDebugTracing) { Log.d(TestProtocol.MISSING_PROMISE_ICON, LOG + " tryQueuePromiseAppIcon" + ", SessionCommitReceiveEnabled" + SessionCommitReceiver.isEnabled(mAppContext) + ", verifySessionInfo(sessionInfo)=" + verifySessionInfo(sessionInfo) + ", !promiseIconAdded=" + (sessionInfo != null && !promiseIconAddedForId(sessionInfo.getSessionId()))); } if (SessionCommitReceiver.isEnabled(mAppContext) && verifySessionInfo(sessionInfo) && !promiseIconAddedForId(sessionInfo.getSessionId())) { FileLog.d(LOG, "Adding package name to install queue: " + sessionInfo.getAppPackageName()); ItemInstallQueue.INSTANCE.get(mAppContext) .queueItem(sessionInfo.getAppPackageName(), getUserHandle(sessionInfo)); getPromiseIconIds().add(sessionInfo.getSessionId()); updatePromiseIconPrefs(); } } public boolean verifySessionInfo(@Nullable final PackageInstaller.SessionInfo sessionInfo) { if (TestProtocol.sDebugTracing) { boolean appNotInstalled = sessionInfo == null || !new PackageManagerHelper(mAppContext) .isAppInstalled(sessionInfo.getAppPackageName(), getUserHandle(sessionInfo)); boolean labelNotEmpty = sessionInfo != null && !TextUtils.isEmpty(sessionInfo.getAppLabel()); Log.d(TestProtocol.MISSING_PROMISE_ICON, LOG + " verifySessionInfo" + ", verify(sessionInfo)=" + verify(sessionInfo) + ", reason=" + (sessionInfo == null ? null : sessionInfo.getInstallReason()) + ", PackageManager.INSTALL_REASON_USER=" + PackageManager.INSTALL_REASON_USER + ", hasIcon=" + (sessionInfo != null && sessionInfo.getAppIcon() != null) + ", label is ! empty=" + labelNotEmpty + " +, app not installed=" + appNotInstalled); } return verify(sessionInfo) != null && sessionInfo.getInstallReason() == PackageManager.INSTALL_REASON_USER && sessionInfo.getAppIcon() != null && !TextUtils.isEmpty(sessionInfo.getAppLabel()) && !new PackageManagerHelper(mAppContext).isAppInstalled( sessionInfo.getAppPackageName(), getUserHandle(sessionInfo)); } public InstallSessionTracker registerInstallTracker( @Nullable final InstallSessionTracker.Callback callback) { InstallSessionTracker tracker = new InstallSessionTracker( this, callback, mInstaller, mLauncherApps); tracker.register(); return tracker; } public static UserHandle getUserHandle(@NonNull final SessionInfo info) { return Utilities.ATLEAST_Q ? info.getUser() : Process.myUserHandle(); } }