1 /* 2 * Copyright (C) 2024 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.server.appsearch.appsindexer; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.app.appsearch.util.LogUtil; 22 import android.content.ContentResolver; 23 import android.content.Intent; 24 import android.content.pm.ActivityInfo; 25 import android.content.pm.ApplicationInfo; 26 import android.content.pm.PackageInfo; 27 import android.content.pm.PackageManager; 28 import android.content.pm.ResolveInfo; 29 import android.content.pm.Signature; 30 import android.content.res.Resources; 31 import android.net.Uri; 32 import android.text.TextUtils; 33 import android.util.ArrayMap; 34 import android.util.Log; 35 36 import com.android.server.appsearch.appsindexer.appsearchtypes.MobileApplication; 37 38 import java.security.MessageDigest; 39 import java.security.NoSuchAlgorithmException; 40 import java.util.ArrayList; 41 import java.util.List; 42 import java.util.Map; 43 import java.util.Objects; 44 45 /** Utility class for pulling apps details from package manager. */ 46 public final class AppsUtil { 47 public static final String TAG = "AppSearchAppsUtil"; 48 AppsUtil()49 private AppsUtil() {} 50 51 /** Gets the resource Uri given a resource id. */ 52 @NonNull getResourceUri( @onNull PackageManager packageManager, @NonNull ApplicationInfo appInfo, int resourceId)53 private static Uri getResourceUri( 54 @NonNull PackageManager packageManager, 55 @NonNull ApplicationInfo appInfo, 56 int resourceId) 57 throws PackageManager.NameNotFoundException { 58 Objects.requireNonNull(packageManager); 59 Objects.requireNonNull(appInfo); 60 Resources resources = packageManager.getResourcesForApplication(appInfo); 61 String resPkg = resources.getResourcePackageName(resourceId); 62 String type = resources.getResourceTypeName(resourceId); 63 return makeResourceUri(appInfo.packageName, resPkg, type, resourceId); 64 } 65 66 /** 67 * Appends the resource id instead of name to make the resource uri due to b/161564466. The 68 * resource names for some apps (e.g. Chrome) are obfuscated due to resource name collapsing, so 69 * we need to use resource id instead. 70 * 71 * @see Uri 72 */ 73 @NonNull makeResourceUri( @onNull String appPkg, @NonNull String resPkg, @NonNull String type, int resourceId)74 private static Uri makeResourceUri( 75 @NonNull String appPkg, @NonNull String resPkg, @NonNull String type, int resourceId) { 76 Objects.requireNonNull(appPkg); 77 Objects.requireNonNull(resPkg); 78 Objects.requireNonNull(type); 79 80 // For more details on Android URIs, see the official Android documentation: 81 // https://developer.android.com/guide/topics/providers/content-provider-basics#ContentURIs 82 Uri.Builder uriBuilder = new Uri.Builder(); 83 uriBuilder.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE); 84 uriBuilder.encodedAuthority(appPkg); 85 uriBuilder.appendEncodedPath(type); 86 if (!appPkg.equals(resPkg)) { 87 uriBuilder.appendEncodedPath(resPkg + ":" + resourceId); 88 } else { 89 uriBuilder.appendEncodedPath(String.valueOf(resourceId)); 90 } 91 return uriBuilder.build(); 92 } 93 94 /** 95 * Gets the icon uri for the activity. 96 * 97 * @return the icon Uri string, or null if there is no icon resource. 98 */ 99 @Nullable getActivityIconUriString( @onNull PackageManager packageManager, @NonNull ActivityInfo activityInfo)100 private static String getActivityIconUriString( 101 @NonNull PackageManager packageManager, @NonNull ActivityInfo activityInfo) { 102 Objects.requireNonNull(packageManager); 103 Objects.requireNonNull(activityInfo); 104 int iconResourceId = activityInfo.getIconResource(); 105 if (iconResourceId == 0) { 106 return null; 107 } 108 109 try { 110 return getResourceUri(packageManager, activityInfo.applicationInfo, iconResourceId) 111 .toString(); 112 } catch (PackageManager.NameNotFoundException e) { 113 // If resources aren't found for the application, that is fine. We return null and 114 // handle it with getActivityIconUriString 115 return null; 116 } 117 } 118 119 /** 120 * Gets {@link PackageInfo}s only for packages that have a launch activity, along with their 121 * corresponding {@link ResolveInfo}. This is useful for building schemas as well as determining 122 * which packages to set schemas for. 123 * 124 * @return a mapping of {@link PackageInfo}s with their corresponding {@link ResolveInfo} for 125 * the packages launch activity. 126 * @see PackageManager#getInstalledPackages 127 * @see PackageManager#queryIntentActivities 128 */ 129 @NonNull getLaunchablePackages( @onNull PackageManager packageManager)130 public static Map<PackageInfo, ResolveInfo> getLaunchablePackages( 131 @NonNull PackageManager packageManager) { 132 Objects.requireNonNull(packageManager); 133 List<PackageInfo> packageInfos = 134 packageManager.getInstalledPackages( 135 PackageManager.GET_META_DATA | PackageManager.GET_SIGNING_CERTIFICATES); 136 Map<PackageInfo, ResolveInfo> launchablePackages = new ArrayMap<>(); 137 Intent intent = new Intent(Intent.ACTION_MAIN, null); 138 intent.addCategory(Intent.CATEGORY_LAUNCHER); 139 intent.setPackage(null); 140 List<ResolveInfo> activities = packageManager.queryIntentActivities(intent, 0); 141 Map<String, ResolveInfo> packageNameToLauncher = new ArrayMap<>(); 142 for (int i = 0; i < activities.size(); i++) { 143 ResolveInfo ri = activities.get(i); 144 packageNameToLauncher.put(ri.activityInfo.packageName, ri); 145 } 146 147 for (int i = 0; i < packageInfos.size(); i++) { 148 PackageInfo packageInfo = packageInfos.get(i); 149 ResolveInfo resolveInfo = packageNameToLauncher.get(packageInfo.packageName); 150 if (resolveInfo != null) { 151 // Include the resolve info as we might need it later to build the MobileApplication 152 launchablePackages.put(packageInfo, resolveInfo); 153 } 154 } 155 156 return launchablePackages; 157 } 158 159 /** 160 * Uses {@link PackageManager} and a Map of {@link PackageInfo}s to {@link ResolveInfo}s to 161 * build AppSearch {@link MobileApplication} documents. Info from both are required to build app 162 * documents. 163 * 164 * @param packageInfos a mapping of {@link PackageInfo}s and their corresponding {@link 165 * ResolveInfo} for the packages launch activity. 166 */ 167 @NonNull buildAppsFromPackageInfos( @onNull PackageManager packageManager, @NonNull Map<PackageInfo, ResolveInfo> packageInfos)168 public static List<MobileApplication> buildAppsFromPackageInfos( 169 @NonNull PackageManager packageManager, 170 @NonNull Map<PackageInfo, ResolveInfo> packageInfos) { 171 Objects.requireNonNull(packageManager); 172 Objects.requireNonNull(packageInfos); 173 174 List<MobileApplication> mobileApplications = new ArrayList<>(); 175 for (Map.Entry<PackageInfo, ResolveInfo> entry : packageInfos.entrySet()) { 176 MobileApplication mobileApplication = 177 createMobileApplication(packageManager, entry.getKey(), entry.getValue()); 178 if (mobileApplication != null && !mobileApplication.getDisplayName().isEmpty()) { 179 mobileApplications.add(mobileApplication); 180 } 181 } 182 return mobileApplications; 183 } 184 185 /** Gets the SHA-256 certificate from a {@link PackageManager}, or null if it is not found */ 186 @Nullable getCertificate(@onNull PackageInfo packageInfo)187 public static byte[] getCertificate(@NonNull PackageInfo packageInfo) { 188 Objects.requireNonNull(packageInfo); 189 if (packageInfo.signingInfo == null) { 190 if (LogUtil.DEBUG) { 191 Log.d(TAG, "Signing info not found for package: " + packageInfo.packageName); 192 } 193 return null; 194 } 195 MessageDigest md; 196 try { 197 md = MessageDigest.getInstance("SHA256"); 198 } catch (NoSuchAlgorithmException e) { 199 return null; 200 } 201 Signature[] signatures = packageInfo.signingInfo.getSigningCertificateHistory(); 202 if (signatures == null || signatures.length == 0) { 203 return null; 204 } 205 md.update(signatures[0].toByteArray()); 206 return md.digest(); 207 } 208 209 /** 210 * Uses PackageManager to supplement packageInfos with an application display name and icon uri. 211 * 212 * @return a MobileApplication representing the packageInfo, null if finding the signing 213 * certificate fails. 214 */ 215 @Nullable createMobileApplication( @onNull PackageManager packageManager, @NonNull PackageInfo packageInfo, @NonNull ResolveInfo resolveInfo)216 private static MobileApplication createMobileApplication( 217 @NonNull PackageManager packageManager, 218 @NonNull PackageInfo packageInfo, 219 @NonNull ResolveInfo resolveInfo) { 220 Objects.requireNonNull(packageManager); 221 Objects.requireNonNull(packageInfo); 222 Objects.requireNonNull(resolveInfo); 223 224 String applicationDisplayName = resolveInfo.loadLabel(packageManager).toString(); 225 if (TextUtils.isEmpty(applicationDisplayName)) { 226 applicationDisplayName = packageInfo.applicationInfo.className; 227 } 228 229 String iconUri = getActivityIconUriString(packageManager, resolveInfo.activityInfo); 230 231 byte[] certificate = getCertificate(packageInfo); 232 if (certificate == null) { 233 return null; 234 } 235 236 MobileApplication.Builder builder = 237 new MobileApplication.Builder(packageInfo.packageName, certificate) 238 .setDisplayName(applicationDisplayName) 239 // TODO(b/275592563): Populate with nicknames from various sources 240 .setCreationTimestampMillis(packageInfo.firstInstallTime) 241 .setUpdatedTimestampMs(packageInfo.lastUpdateTime); 242 243 String applicationLabel = 244 packageManager.getApplicationLabel(packageInfo.applicationInfo).toString(); 245 if (!applicationDisplayName.equals(applicationLabel)) { 246 // This can be different from applicationDisplayName, and should be indexed 247 builder.setAlternateNames(applicationLabel); 248 } 249 250 if (iconUri != null) { 251 builder.setIconUri(iconUri); 252 } 253 254 if (resolveInfo.activityInfo.name != null) { 255 builder.setClassName(resolveInfo.activityInfo.name); 256 } 257 return builder.build(); 258 } 259 } 260 261