Home | History | Annotate | Download | only in search2
      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