1 /* 2 * Copyright (C) 2017 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 18 package com.android.settings.search; 19 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.pm.ApplicationInfo; 23 import android.content.pm.PackageManager; 24 import android.content.pm.ResolveInfo; 25 import android.content.pm.UserInfo; 26 import android.net.Uri; 27 import android.os.UserHandle; 28 import android.os.UserManager; 29 import android.provider.Settings; 30 import android.support.annotation.VisibleForTesting; 31 import android.text.TextUtils; 32 33 import com.android.internal.logging.nano.MetricsProto; 34 import com.android.settings.R; 35 import com.android.settings.SettingsActivity; 36 import com.android.settings.applications.ManageApplications; 37 import com.android.settings.applications.PackageManagerWrapper; 38 import com.android.settings.dashboard.SiteMapManager; 39 import com.android.settings.utils.AsyncLoader; 40 41 import java.util.ArrayList; 42 import java.util.HashSet; 43 import java.util.List; 44 import java.util.Objects; 45 import java.util.Set; 46 47 /** 48 * Search loader for installed apps. 49 */ 50 public class InstalledAppResultLoader extends AsyncLoader<Set<? extends SearchResult>> { 51 52 private static final int NAME_NO_MATCH = -1; 53 private static final Intent LAUNCHER_PROBE = new Intent(Intent.ACTION_MAIN) 54 .addCategory(Intent.CATEGORY_LAUNCHER); 55 56 private List<String> mBreadcrumb; 57 private SiteMapManager mSiteMapManager; 58 @VisibleForTesting 59 final String mQuery; 60 private final UserManager mUserManager; 61 private final PackageManagerWrapper mPackageManager; 62 private final List<ResolveInfo> mHomeActivities = new ArrayList<>(); 63 InstalledAppResultLoader(Context context, PackageManagerWrapper pmWrapper, String query, SiteMapManager mapManager)64 public InstalledAppResultLoader(Context context, PackageManagerWrapper pmWrapper, 65 String query, SiteMapManager mapManager) { 66 super(context); 67 mSiteMapManager = mapManager; 68 mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); 69 mPackageManager = pmWrapper; 70 mQuery = query; 71 } 72 73 @Override loadInBackground()74 public Set<? extends SearchResult> loadInBackground() { 75 final Set<AppSearchResult> results = new HashSet<>(); 76 final PackageManager pm = mPackageManager.getPackageManager(); 77 78 mHomeActivities.clear(); 79 mPackageManager.getHomeActivities(mHomeActivities); 80 81 for (UserInfo user : getUsersToCount()) { 82 final List<ApplicationInfo> apps = 83 mPackageManager.getInstalledApplicationsAsUser( 84 PackageManager.MATCH_DISABLED_COMPONENTS 85 | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS 86 | (user.isAdmin() ? PackageManager.MATCH_ANY_USER : 0), 87 user.id); 88 for (ApplicationInfo info : apps) { 89 if (!shouldIncludeAsCandidate(info, user)) { 90 continue; 91 } 92 final CharSequence label = info.loadLabel(pm); 93 final int wordDiff = getWordDifference(label.toString(), mQuery); 94 if (wordDiff == NAME_NO_MATCH) { 95 continue; 96 } 97 final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) 98 .setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) 99 .setData(Uri.fromParts("package", info.packageName, null)) 100 .putExtra(SettingsActivity.EXTRA_SOURCE_METRICS_CATEGORY, 101 MetricsProto.MetricsEvent.DASHBOARD_SEARCH_RESULTS); 102 103 final AppSearchResult.Builder builder = new AppSearchResult.Builder(); 104 builder.setAppInfo(info) 105 .setStableId(Objects.hash(info.packageName, user.id)) 106 .setTitle(info.loadLabel(pm)) 107 .setRank(getRank(wordDiff)) 108 .addBreadcrumbs(getBreadCrumb()) 109 .setPayload(new ResultPayload(intent)); 110 results.add(builder.build()); 111 } 112 } 113 return results; 114 } 115 116 /** 117 * Returns true if the candidate should be included in candidate list 118 * <p/> 119 * This method matches logic in {@code ApplicationState#FILTER_DOWNLOADED_AND_LAUNCHER}. 120 */ shouldIncludeAsCandidate(ApplicationInfo info, UserInfo user)121 private boolean shouldIncludeAsCandidate(ApplicationInfo info, UserInfo user) { 122 // Not system app 123 if ((info.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0 124 || (info.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { 125 return true; 126 } 127 // Shows up in launcher 128 final Intent launchIntent = new Intent(LAUNCHER_PROBE) 129 .setPackage(info.packageName); 130 final List<ResolveInfo> intents = mPackageManager.queryIntentActivitiesAsUser( 131 launchIntent, 132 PackageManager.MATCH_DISABLED_COMPONENTS 133 | PackageManager.MATCH_DIRECT_BOOT_AWARE 134 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, 135 user.id); 136 if (intents != null && intents.size() != 0) { 137 return true; 138 } 139 // Is launcher app itself 140 return isPackageInList(mHomeActivities, info.packageName); 141 } 142 143 @Override onDiscardResult(Set<? extends SearchResult> result)144 protected void onDiscardResult(Set<? extends SearchResult> result) { 145 146 } 147 getUsersToCount()148 private List<UserInfo> getUsersToCount() { 149 return mUserManager.getProfiles(UserHandle.myUserId()); 150 } 151 152 /** 153 * Returns "difference" between appName and query string. appName must contain all 154 * characters from query as a prefix to a word, in the same order. 155 * If not, returns NAME_NO_MATCH. 156 * If they do match, returns an int value representing how different they are, 157 * and larger values means they are less similar. 158 * <p/> 159 * Example: 160 * appName: Abcde, query: Abcde, Returns 0 161 * appName: Abcde, query: abc, Returns 2 162 * appName: Abcde, query: ab, Returns 3 163 * appName: Abcde, query: bc, Returns NAME_NO_MATCH 164 * appName: Abcde, query: xyz, Returns NAME_NO_MATCH 165 * appName: Abc de, query: de, Returns 4 166 * TODO: Move this to a common util class. 167 */ getWordDifference(String appName, String query)168 static int getWordDifference(String appName, String query) { 169 if (TextUtils.isEmpty(appName) || TextUtils.isEmpty(query)) { 170 return NAME_NO_MATCH; 171 } 172 173 final char[] queryTokens = query.toLowerCase().toCharArray(); 174 final char[] appTokens = appName.toLowerCase().toCharArray(); 175 final int appLength = appTokens.length; 176 if (queryTokens.length > appLength) { 177 return NAME_NO_MATCH; 178 } 179 180 int i = 0; 181 int j; 182 183 while (i < appLength) { 184 j = 0; 185 // Currently matching a prefix 186 while ((i + j < appLength) && (queryTokens[j] == appTokens[i + j])) { 187 // Matched the entire query 188 if (++j >= queryTokens.length) { 189 // Use the diff in length as a proxy of how close the 2 words match. 190 // Value range from 0 to infinity. 191 return appLength - queryTokens.length; 192 } 193 } 194 195 i += j; 196 197 // Remaining string is longer that the query or we have search the whole app name. 198 if (queryTokens.length > appLength - i) { 199 return NAME_NO_MATCH; 200 } 201 202 // This is the first index where app name and query name are different 203 // Find the next space in the app name or the end of the app name. 204 while ((i < appLength) && (!Character.isWhitespace(appTokens[i++]))) ; 205 206 // Find the start of the next word 207 while ((i < appLength) && !(Character.isLetter(appTokens[i]) 208 || Character.isDigit(appTokens[i]))) { 209 // Increment in body because we cannot guarantee which condition was true 210 i++; 211 } 212 } 213 return NAME_NO_MATCH; 214 } 215 isPackageInList(List<ResolveInfo> resolveInfos, String pkg)216 private boolean isPackageInList(List<ResolveInfo> resolveInfos, String pkg) { 217 for (ResolveInfo info : resolveInfos) { 218 if (TextUtils.equals(info.activityInfo.packageName, pkg)) { 219 return true; 220 } 221 } 222 return false; 223 } 224 getBreadCrumb()225 private List<String> getBreadCrumb() { 226 if (mBreadcrumb == null || mBreadcrumb.isEmpty()) { 227 final Context context = getContext(); 228 mBreadcrumb = mSiteMapManager.buildBreadCrumb( 229 context, ManageApplications.class.getName(), 230 context.getString(R.string.applications_settings)); 231 } 232 return mBreadcrumb; 233 } 234 235 /** 236 * A temporary ranking scheme for installed apps. 237 * 238 * @param wordDiff difference between query length and app name length. 239 * @return the ranking. 240 */ getRank(int wordDiff)241 private int getRank(int wordDiff) { 242 if (wordDiff < 6) { 243 return 2; 244 } 245 return 3; 246 } 247 } 248