1 /* 2 * Copyright (C) 2016 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.util; 18 19 import static android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI; 20 21 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE; 22 23 import android.content.ActivityNotFoundException; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.pm.ApplicationInfo; 28 import android.content.pm.LauncherActivityInfo; 29 import android.content.pm.LauncherApps; 30 import android.content.pm.PackageInfo; 31 import android.content.pm.PackageManager; 32 import android.content.pm.PackageManager.NameNotFoundException; 33 import android.content.pm.ResolveInfo; 34 import android.graphics.Rect; 35 import android.os.Bundle; 36 import android.os.Process; 37 import android.os.UserHandle; 38 import android.text.TextUtils; 39 import android.util.Log; 40 import android.widget.Toast; 41 42 import androidx.annotation.NonNull; 43 import androidx.annotation.Nullable; 44 45 import com.android.launcher3.Flags; 46 import com.android.launcher3.PendingAddItemInfo; 47 import com.android.launcher3.R; 48 import com.android.launcher3.Utilities; 49 import com.android.launcher3.model.data.AppInfo; 50 import com.android.launcher3.model.data.ItemInfo; 51 import com.android.launcher3.model.data.ItemInfoWithIcon; 52 import com.android.launcher3.model.data.LauncherAppWidgetInfo; 53 import com.android.launcher3.model.data.WorkspaceItemInfo; 54 55 import java.util.List; 56 import java.util.Objects; 57 58 /** 59 * Utility methods using package manager 60 */ 61 public class PackageManagerHelper implements SafeCloseable{ 62 63 private static final String TAG = "PackageManagerHelper"; 64 65 @NonNull 66 public static final MainThreadInitializedObject<PackageManagerHelper> INSTANCE = 67 new MainThreadInitializedObject<>(PackageManagerHelper::new); 68 69 @NonNull 70 private final Context mContext; 71 72 @NonNull 73 private final PackageManager mPm; 74 75 @NonNull 76 private final LauncherApps mLauncherApps; 77 78 private final String[] mLegacyMultiInstanceSupportedApps; 79 PackageManagerHelper(@onNull final Context context)80 public PackageManagerHelper(@NonNull final Context context) { 81 mContext = context; 82 mPm = context.getPackageManager(); 83 mLauncherApps = Objects.requireNonNull(context.getSystemService(LauncherApps.class)); 84 mLegacyMultiInstanceSupportedApps = mContext.getResources().getStringArray( 85 R.array.config_appsSupportMultiInstancesSplit); 86 } 87 88 @Override close()89 public void close() { } 90 91 /** 92 * Returns true if the app can possibly be on the SDCard. This is just a workaround and doesn't 93 * guarantee that the app is on SD card. 94 */ isAppOnSdcard(@onNull final String packageName, @NonNull final UserHandle user)95 public boolean isAppOnSdcard(@NonNull final String packageName, 96 @NonNull final UserHandle user) { 97 final ApplicationInfo info = getApplicationInfo( 98 packageName, user, PackageManager.MATCH_UNINSTALLED_PACKAGES); 99 return info != null && (info.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0; 100 } 101 102 /** 103 * Returns whether the target app is suspended for a given user as per 104 * {@link android.app.admin.DevicePolicyManager#isPackageSuspended}. 105 */ isAppSuspended(@onNull final String packageName, @NonNull final UserHandle user)106 public boolean isAppSuspended(@NonNull final String packageName, 107 @NonNull final UserHandle user) { 108 final ApplicationInfo info = getApplicationInfo(packageName, user, 0); 109 return info != null && isAppSuspended(info); 110 } 111 112 /** 113 * Returns whether the target app is installed for a given user 114 */ isAppInstalled(@onNull final String packageName, @NonNull final UserHandle user)115 public boolean isAppInstalled(@NonNull final String packageName, 116 @NonNull final UserHandle user) { 117 final ApplicationInfo info = getApplicationInfo(packageName, user, 0); 118 return info != null; 119 } 120 121 /** 122 * Returns whether the target app is archived for a given user 123 */ 124 @SuppressWarnings("NewApi") isAppArchivedForUser(@onNull final String packageName, @NonNull final UserHandle user)125 public boolean isAppArchivedForUser(@NonNull final String packageName, 126 @NonNull final UserHandle user) { 127 if (!Flags.enableSupportForArchiving()) { 128 return false; 129 } 130 final ApplicationInfo info = getApplicationInfo( 131 // LauncherApps does not support long flags currently. Since archived apps are 132 // subset of uninstalled apps, this filter also includes archived apps. 133 packageName, user, PackageManager.MATCH_UNINSTALLED_PACKAGES); 134 return info != null && info.isArchived; 135 } 136 137 /** 138 * Returns whether the target app is in archived state 139 */ 140 @SuppressWarnings("NewApi") isAppArchived(@onNull final String packageName)141 public boolean isAppArchived(@NonNull final String packageName) { 142 final ApplicationInfo info; 143 try { 144 info = mPm.getPackageInfo(packageName, 145 PackageManager.PackageInfoFlags.of( 146 PackageManager.MATCH_ARCHIVED_PACKAGES)).applicationInfo; 147 return info.isArchived; 148 } catch (NameNotFoundException e) { 149 Log.e(TAG, "Failed to get applicationInfo for package: " + packageName, e); 150 return false; 151 } 152 } 153 154 /** 155 * Returns the installing app package for the given package 156 */ getAppInstallerPackage(@onNull final String packageName)157 public String getAppInstallerPackage(@NonNull final String packageName) { 158 try { 159 return mPm.getInstallSourceInfo(packageName).getInstallingPackageName(); 160 } catch (NameNotFoundException e) { 161 Log.e(TAG, "Failed to get installer package for app package:" + packageName, e); 162 return null; 163 } 164 } 165 166 /** 167 * Returns the application info for the provided package or null 168 */ 169 @Nullable getApplicationInfo(@onNull final String packageName, @NonNull final UserHandle user, final int flags)170 public ApplicationInfo getApplicationInfo(@NonNull final String packageName, 171 @NonNull final UserHandle user, final int flags) { 172 try { 173 ApplicationInfo info = mLauncherApps.getApplicationInfo(packageName, flags, user); 174 return !isPackageInstalledOrArchived(info) || !info.enabled ? null : info; 175 } catch (PackageManager.NameNotFoundException e) { 176 return null; 177 } 178 } 179 180 /** 181 * Returns the preferred launch activity intent for a given package. 182 */ 183 @Nullable getAppLaunchIntent(@ullable final String pkg, @NonNull final UserHandle user)184 public Intent getAppLaunchIntent(@Nullable final String pkg, @NonNull final UserHandle user) { 185 LauncherActivityInfo info = getAppLaunchInfo(pkg, user); 186 return info != null ? AppInfo.makeLaunchIntent(info) : null; 187 } 188 189 /** 190 * Returns the preferred launch activity for a given package. 191 */ 192 @Nullable getAppLaunchInfo(@ullable final String pkg, @NonNull final UserHandle user)193 public LauncherActivityInfo getAppLaunchInfo(@Nullable final String pkg, 194 @NonNull final UserHandle user) { 195 List<LauncherActivityInfo> activities = mLauncherApps.getActivityList(pkg, user); 196 return activities.isEmpty() ? null : activities.get(0); 197 } 198 199 /** 200 * Returns whether an application is suspended as per 201 * {@link android.app.admin.DevicePolicyManager#isPackageSuspended}. 202 */ isAppSuspended(ApplicationInfo info)203 public static boolean isAppSuspended(ApplicationInfo info) { 204 return (info.flags & ApplicationInfo.FLAG_SUSPENDED) != 0; 205 } 206 207 /** 208 * Starts the details activity for {@code info} 209 */ startDetailsActivityForInfo(Context context, ItemInfo info, Rect sourceBounds, Bundle opts)210 public static void startDetailsActivityForInfo(Context context, ItemInfo info, 211 Rect sourceBounds, Bundle opts) { 212 if (info instanceof ItemInfoWithIcon appInfo 213 && (appInfo.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0) { 214 context.startActivity(ApiWrapper.INSTANCE.get(context).getAppMarketActivityIntent( 215 appInfo.getTargetComponent().getPackageName(), Process.myUserHandle())); 216 return; 217 } 218 ComponentName componentName = null; 219 if (info instanceof AppInfo) { 220 componentName = ((AppInfo) info).componentName; 221 } else if (info instanceof WorkspaceItemInfo) { 222 componentName = info.getTargetComponent(); 223 } else if (info instanceof PendingAddItemInfo) { 224 componentName = ((PendingAddItemInfo) info).componentName; 225 } else if (info instanceof LauncherAppWidgetInfo) { 226 componentName = ((LauncherAppWidgetInfo) info).providerName; 227 } 228 if (componentName != null) { 229 try { 230 context.getSystemService(LauncherApps.class).startAppDetailsActivity(componentName, 231 info.user, sourceBounds, opts); 232 } catch (SecurityException | ActivityNotFoundException e) { 233 Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); 234 Log.e(TAG, "Unable to launch settings", e); 235 } 236 } 237 } 238 isSystemApp(@onNull final Context context, @NonNull final Intent intent)239 public static boolean isSystemApp(@NonNull final Context context, 240 @NonNull final Intent intent) { 241 PackageManager pm = context.getPackageManager(); 242 ComponentName cn = intent.getComponent(); 243 String packageName = null; 244 if (cn == null) { 245 ResolveInfo info = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); 246 if ((info != null) && (info.activityInfo != null)) { 247 packageName = info.activityInfo.packageName; 248 } 249 } else { 250 packageName = cn.getPackageName(); 251 } 252 if (packageName == null) { 253 packageName = intent.getPackage(); 254 } 255 if (packageName != null) { 256 try { 257 PackageInfo info = pm.getPackageInfo(packageName, 0); 258 return (info != null) && (info.applicationInfo != null) && 259 ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0); 260 } catch (NameNotFoundException e) { 261 return false; 262 } 263 } else { 264 return false; 265 } 266 } 267 268 /** 269 * Returns true if the intent is a valid launch intent for a launcher activity of an app. 270 * This is used to identify shortcuts which are different from the ones exposed by the 271 * applications' manifest file. 272 * 273 * @param launchIntent The intent that will be launched when the shortcut is clicked. 274 */ isLauncherAppTarget(Intent launchIntent)275 public static boolean isLauncherAppTarget(Intent launchIntent) { 276 if (launchIntent != null 277 && Intent.ACTION_MAIN.equals(launchIntent.getAction()) 278 && launchIntent.getComponent() != null 279 && launchIntent.getCategories() != null 280 && launchIntent.getCategories().size() == 1 281 && launchIntent.hasCategory(Intent.CATEGORY_LAUNCHER) 282 && TextUtils.isEmpty(launchIntent.getDataString())) { 283 // An app target can either have no extra or have ItemInfo.EXTRA_PROFILE. 284 Bundle extras = launchIntent.getExtras(); 285 return extras == null || extras.keySet().isEmpty(); 286 } 287 return false; 288 } 289 290 /** 291 * Returns true if Launcher has the permission to access shortcuts. 292 * 293 * @see LauncherApps#hasShortcutHostPermission() 294 */ hasShortcutsPermission(Context context)295 public static boolean hasShortcutsPermission(Context context) { 296 try { 297 return context.getSystemService(LauncherApps.class).hasShortcutHostPermission(); 298 } catch (SecurityException | IllegalStateException e) { 299 Log.e(TAG, "Failed to make shortcut manager call", e); 300 } 301 return false; 302 } 303 304 /** Returns the incremental download progress for the given shortcut's app. */ getLoadingProgress(LauncherActivityInfo info)305 public static int getLoadingProgress(LauncherActivityInfo info) { 306 if (Utilities.ATLEAST_S) { 307 return (int) (100 * info.getLoadingProgress()); 308 } 309 return 100; 310 } 311 312 /** Returns true in case app is installed on the device or in archived state. */ 313 @SuppressWarnings("NewApi") isPackageInstalledOrArchived(ApplicationInfo info)314 private boolean isPackageInstalledOrArchived(ApplicationInfo info) { 315 return (info.flags & ApplicationInfo.FLAG_INSTALLED) != 0 || ( 316 Flags.enableSupportForArchiving() && info.isArchived); 317 } 318 319 /** 320 * Returns whether the given component or its application has the multi-instance property set. 321 */ supportsMultiInstance(@onNull ComponentName component)322 public boolean supportsMultiInstance(@NonNull ComponentName component) { 323 // Check the legacy hardcoded allowlist first 324 for (String pkg : mLegacyMultiInstanceSupportedApps) { 325 if (pkg.equals(component.getPackageName())) { 326 return true; 327 } 328 } 329 330 // Check app multi-instance properties after V 331 if (!Utilities.ATLEAST_V) { 332 return false; 333 } 334 335 try { 336 // Check if the component has the multi-instance property 337 return mPm.getProperty(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, component) 338 .getBoolean(); 339 } catch (PackageManager.NameNotFoundException e1) { 340 try { 341 // Check if the application has the multi-instance property 342 return mPm.getProperty(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, 343 component.getPackageName()) 344 .getBoolean(); 345 } catch (PackageManager.NameNotFoundException e2) { 346 // Fall through 347 } 348 } 349 return false; 350 } 351 352 /** 353 * Returns whether two apps should be considered the same for multi-instance purposes, which 354 * requires additional checks to ensure they can be started as multiple instances. 355 */ isSameAppForMultiInstance(@onNull ItemInfo app1, @NonNull ItemInfo app2)356 public static boolean isSameAppForMultiInstance(@NonNull ItemInfo app1, 357 @NonNull ItemInfo app2) { 358 return app1.getTargetPackage().equals(app2.getTargetPackage()) 359 && app1.user.equals(app2.user); 360 } 361 } 362