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