1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3.pm; 18 19 import android.content.Context; 20 import android.content.pm.ApplicationInfo; 21 import android.content.pm.LauncherApps; 22 import android.content.pm.PackageInstaller; 23 import android.content.pm.PackageInstaller.SessionInfo; 24 import android.content.pm.PackageManager; 25 import android.os.Process; 26 import android.os.UserHandle; 27 import android.text.TextUtils; 28 import android.util.Log; 29 30 import androidx.annotation.NonNull; 31 import androidx.annotation.Nullable; 32 import androidx.annotation.RequiresApi; 33 import androidx.annotation.WorkerThread; 34 35 import com.android.launcher3.LauncherPrefs; 36 import com.android.launcher3.LauncherSettings; 37 import com.android.launcher3.SessionCommitReceiver; 38 import com.android.launcher3.Utilities; 39 import com.android.launcher3.config.FeatureFlags; 40 import com.android.launcher3.logging.FileLog; 41 import com.android.launcher3.model.ItemInstallQueue; 42 import com.android.launcher3.testing.shared.TestProtocol; 43 import com.android.launcher3.util.IntArray; 44 import com.android.launcher3.util.IntSet; 45 import com.android.launcher3.util.MainThreadInitializedObject; 46 import com.android.launcher3.util.PackageManagerHelper; 47 import com.android.launcher3.util.PackageUserKey; 48 import com.android.launcher3.util.Preconditions; 49 50 import java.util.ArrayList; 51 import java.util.HashMap; 52 import java.util.Iterator; 53 import java.util.List; 54 import java.util.Objects; 55 56 /** 57 * Utility class to tracking install sessions 58 */ 59 public class InstallSessionHelper { 60 61 @NonNull 62 private static final String LOG = "InstallSessionHelper"; 63 64 // Set<String> of session ids of promise icons that have been added to the home screen 65 // as FLAG_PROMISE_NEW_INSTALLS. 66 @NonNull 67 public static final String PROMISE_ICON_IDS = "promise_icon_ids"; 68 69 private static final boolean DEBUG = false; 70 71 @NonNull 72 public static final MainThreadInitializedObject<InstallSessionHelper> INSTANCE = 73 new MainThreadInitializedObject<>(InstallSessionHelper::new); 74 75 @Nullable 76 private final LauncherApps mLauncherApps; 77 78 @NonNull 79 private final Context mAppContext; 80 81 @NonNull 82 private final PackageInstaller mInstaller; 83 84 @NonNull 85 private final HashMap<String, Boolean> mSessionVerifiedMap = new HashMap<>(); 86 87 @Nullable 88 private IntSet mPromiseIconIds; 89 InstallSessionHelper(@onNull final Context context)90 public InstallSessionHelper(@NonNull final Context context) { 91 mInstaller = context.getPackageManager().getPackageInstaller(); 92 mAppContext = context.getApplicationContext(); 93 mLauncherApps = context.getSystemService(LauncherApps.class); 94 } 95 96 @WorkerThread 97 @NonNull getPromiseIconIds()98 private IntSet getPromiseIconIds() { 99 Preconditions.assertWorkerThread(); 100 if (mPromiseIconIds != null) { 101 return mPromiseIconIds; 102 } 103 mPromiseIconIds = IntSet.wrap(IntArray.fromConcatString( 104 LauncherPrefs.get(mAppContext).get(LauncherPrefs.PROMISE_ICON_IDS))); 105 106 IntArray existingIds = new IntArray(); 107 for (SessionInfo info : getActiveSessions().values()) { 108 existingIds.add(info.getSessionId()); 109 } 110 IntArray idsToRemove = new IntArray(); 111 112 for (int i = mPromiseIconIds.size() - 1; i >= 0; --i) { 113 if (!existingIds.contains(mPromiseIconIds.getArray().get(i))) { 114 idsToRemove.add(mPromiseIconIds.getArray().get(i)); 115 } 116 } 117 for (int i = idsToRemove.size() - 1; i >= 0; --i) { 118 mPromiseIconIds.getArray().removeValue(idsToRemove.get(i)); 119 } 120 return mPromiseIconIds; 121 } 122 123 @NonNull getActiveSessions()124 public HashMap<PackageUserKey, SessionInfo> getActiveSessions() { 125 HashMap<PackageUserKey, SessionInfo> activePackages = new HashMap<>(); 126 for (SessionInfo info : getAllVerifiedSessions()) { 127 activePackages.put(new PackageUserKey(info.getAppPackageName(), getUserHandle(info)), 128 info); 129 } 130 return activePackages; 131 } 132 133 @Nullable getActiveSessionInfo(UserHandle user, String pkg)134 public SessionInfo getActiveSessionInfo(UserHandle user, String pkg) { 135 for (SessionInfo info : getAllVerifiedSessions()) { 136 boolean match = pkg.equals(info.getAppPackageName()); 137 if (Utilities.ATLEAST_Q && !user.equals(getUserHandle(info))) { 138 match = false; 139 } 140 if (match) { 141 return info; 142 } 143 } 144 return null; 145 } 146 updatePromiseIconPrefs()147 private void updatePromiseIconPrefs() { 148 LauncherPrefs.get(mAppContext).put(LauncherPrefs.PROMISE_ICON_IDS, 149 getPromiseIconIds().getArray().toConcatString()); 150 } 151 152 @Nullable getVerifiedSessionInfo(final int sessionId)153 SessionInfo getVerifiedSessionInfo(final int sessionId) { 154 return verify(mInstaller.getSessionInfo(sessionId)); 155 } 156 157 @Nullable verify(@ullable final SessionInfo sessionInfo)158 private SessionInfo verify(@Nullable final SessionInfo sessionInfo) { 159 if (sessionInfo == null 160 || sessionInfo.getInstallerPackageName() == null 161 || TextUtils.isEmpty(sessionInfo.getAppPackageName())) { 162 if (TestProtocol.sDebugTracing) { 163 Log.d(TestProtocol.MISSING_PROMISE_ICON, LOG + " verify" 164 + ", info=" + (sessionInfo == null) 165 + ", info install name" + (sessionInfo == null 166 ? null 167 : sessionInfo.getInstallerPackageName()) 168 + ", empty pkg name" + TextUtils.isEmpty((sessionInfo == null 169 ? null 170 : sessionInfo.getAppPackageName()))); 171 } 172 return null; 173 } 174 return isTrustedPackage(sessionInfo.getInstallerPackageName(), getUserHandle(sessionInfo)) 175 ? sessionInfo : null; 176 } 177 178 /** 179 * Returns true if the provided packageName can be trusted for user configurations 180 */ isTrustedPackage(String pkg, UserHandle user)181 public boolean isTrustedPackage(String pkg, UserHandle user) { 182 synchronized (mSessionVerifiedMap) { 183 if (!mSessionVerifiedMap.containsKey(pkg)) { 184 boolean hasSystemFlag = new PackageManagerHelper(mAppContext).getApplicationInfo( 185 pkg, user, ApplicationInfo.FLAG_SYSTEM) != null; 186 mSessionVerifiedMap.put(pkg, DEBUG || hasSystemFlag); 187 } 188 } 189 return mSessionVerifiedMap.get(pkg); 190 } 191 192 @NonNull getAllVerifiedSessions()193 public List<SessionInfo> getAllVerifiedSessions() { 194 List<SessionInfo> list = new ArrayList<>(Utilities.ATLEAST_Q 195 ? Objects.requireNonNull(mLauncherApps).getAllPackageInstallerSessions() 196 : mInstaller.getAllSessions()); 197 Iterator<SessionInfo> it = list.iterator(); 198 while (it.hasNext()) { 199 if (verify(it.next()) == null) { 200 it.remove(); 201 } 202 } 203 return list; 204 } 205 206 /** 207 * Attempt to restore workspace layout if the session is triggered due to device restore. 208 */ restoreDbIfApplicable(@onNull final SessionInfo info)209 public boolean restoreDbIfApplicable(@NonNull final SessionInfo info) { 210 if (!FeatureFlags.ENABLE_DATABASE_RESTORE.get()) { 211 return false; 212 } 213 if (isRestore(info)) { 214 LauncherSettings.Settings.call(mAppContext.getContentResolver(), 215 LauncherSettings.Settings.METHOD_RESTORE_BACKUP_TABLE); 216 return true; 217 } 218 return false; 219 } 220 221 @RequiresApi(26) isRestore(@onNull final SessionInfo info)222 private static boolean isRestore(@NonNull final SessionInfo info) { 223 return info.getInstallReason() == PackageManager.INSTALL_REASON_DEVICE_RESTORE; 224 } 225 226 @WorkerThread promiseIconAddedForId(final int sessionId)227 public boolean promiseIconAddedForId(final int sessionId) { 228 return getPromiseIconIds().contains(sessionId); 229 } 230 231 @WorkerThread removePromiseIconId(final int sessionId)232 public void removePromiseIconId(final int sessionId) { 233 if (promiseIconAddedForId(sessionId)) { 234 getPromiseIconIds().getArray().removeValue(sessionId); 235 updatePromiseIconPrefs(); 236 } 237 } 238 239 /** 240 * Add a promise app icon to the workspace iff: 241 * - The settings for it are enabled 242 * - The user installed the app 243 * - There is an app icon and label (For apps with no launching activity, no icon is provided). 244 * - The app is not already installed 245 * - A promise icon for the session has not already been created 246 */ 247 @WorkerThread tryQueuePromiseAppIcon(@ullable final PackageInstaller.SessionInfo sessionInfo)248 void tryQueuePromiseAppIcon(@Nullable final PackageInstaller.SessionInfo sessionInfo) { 249 if (TestProtocol.sDebugTracing) { 250 Log.d(TestProtocol.MISSING_PROMISE_ICON, LOG + " tryQueuePromiseAppIcon" 251 + ", SessionCommitReceiveEnabled" + SessionCommitReceiver.isEnabled(mAppContext) 252 + ", verifySessionInfo(sessionInfo)=" + verifySessionInfo(sessionInfo) 253 + ", !promiseIconAdded=" + (sessionInfo != null 254 && !promiseIconAddedForId(sessionInfo.getSessionId()))); 255 } 256 if (SessionCommitReceiver.isEnabled(mAppContext) 257 && verifySessionInfo(sessionInfo) 258 && !promiseIconAddedForId(sessionInfo.getSessionId())) { 259 FileLog.d(LOG, "Adding package name to install queue: " 260 + sessionInfo.getAppPackageName()); 261 262 ItemInstallQueue.INSTANCE.get(mAppContext) 263 .queueItem(sessionInfo.getAppPackageName(), getUserHandle(sessionInfo)); 264 265 getPromiseIconIds().add(sessionInfo.getSessionId()); 266 updatePromiseIconPrefs(); 267 } 268 } 269 verifySessionInfo(@ullable final PackageInstaller.SessionInfo sessionInfo)270 public boolean verifySessionInfo(@Nullable final PackageInstaller.SessionInfo sessionInfo) { 271 if (TestProtocol.sDebugTracing) { 272 boolean appNotInstalled = sessionInfo == null 273 || !new PackageManagerHelper(mAppContext) 274 .isAppInstalled(sessionInfo.getAppPackageName(), getUserHandle(sessionInfo)); 275 boolean labelNotEmpty = sessionInfo != null 276 && !TextUtils.isEmpty(sessionInfo.getAppLabel()); 277 Log.d(TestProtocol.MISSING_PROMISE_ICON, LOG + " verifySessionInfo" 278 + ", verify(sessionInfo)=" + verify(sessionInfo) 279 + ", reason=" + (sessionInfo == null ? null : sessionInfo.getInstallReason()) 280 + ", PackageManager.INSTALL_REASON_USER=" + PackageManager.INSTALL_REASON_USER 281 + ", hasIcon=" + (sessionInfo != null && sessionInfo.getAppIcon() != null) 282 + ", label is ! empty=" + labelNotEmpty 283 + " +, app not installed=" + appNotInstalled); 284 } 285 return verify(sessionInfo) != null 286 && sessionInfo.getInstallReason() == PackageManager.INSTALL_REASON_USER 287 && sessionInfo.getAppIcon() != null 288 && !TextUtils.isEmpty(sessionInfo.getAppLabel()) 289 && !new PackageManagerHelper(mAppContext).isAppInstalled( 290 sessionInfo.getAppPackageName(), getUserHandle(sessionInfo)); 291 } 292 registerInstallTracker( @ullable final InstallSessionTracker.Callback callback)293 public InstallSessionTracker registerInstallTracker( 294 @Nullable final InstallSessionTracker.Callback callback) { 295 InstallSessionTracker tracker = new InstallSessionTracker( 296 this, callback, mInstaller, mLauncherApps); 297 tracker.register(); 298 return tracker; 299 } 300 getUserHandle(@onNull final SessionInfo info)301 public static UserHandle getUserHandle(@NonNull final SessionInfo info) { 302 return Utilities.ATLEAST_Q ? info.getUser() : Process.myUserHandle(); 303 } 304 } 305