• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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