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