Home | History | Annotate | Download | only in search
      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.os.Handler;
     22 import android.os.Looper;
     23 import android.os.Message;
     24 import android.support.annotation.IntDef;
     25 import android.support.annotation.MainThread;
     26 import android.support.annotation.VisibleForTesting;
     27 import android.support.v7.util.DiffUtil;
     28 import android.support.v7.widget.RecyclerView;
     29 import android.util.ArrayMap;
     30 import android.util.Log;
     31 import android.util.Pair;
     32 import android.view.LayoutInflater;
     33 import android.view.View;
     34 import android.view.ViewGroup;
     35 
     36 import com.android.settings.R;
     37 import com.android.settings.search.ranking.SearchResultsRankerCallback;
     38 
     39 import java.lang.annotation.Retention;
     40 import java.lang.annotation.RetentionPolicy;
     41 import java.util.ArrayList;
     42 import java.util.Collections;
     43 import java.util.Comparator;
     44 import java.util.HashSet;
     45 import java.util.List;
     46 import java.util.Map;
     47 import java.util.Set;
     48 import java.util.TreeSet;
     49 
     50 public class SearchResultsAdapter extends RecyclerView.Adapter<SearchViewHolder>
     51         implements SearchResultsRankerCallback {
     52     private static final String TAG = "SearchResultsAdapter";
     53 
     54     @VisibleForTesting
     55     static final String DB_RESULTS_LOADER_KEY = DatabaseResultLoader.class.getName();
     56 
     57     @VisibleForTesting
     58     static final String APP_RESULTS_LOADER_KEY = InstalledAppResultLoader.class.getName();
     59     @VisibleForTesting
     60     static final String ACCESSIBILITY_LOADER_KEY = AccessibilityServiceResultLoader.class.getName();
     61     @VisibleForTesting
     62     static final String INPUT_DEVICE_LOADER_KEY = InputDeviceResultLoader.class.getName();
     63 
     64     @VisibleForTesting
     65     static final int MSG_RANKING_TIMED_OUT = 1;
     66 
     67     private final SearchFragment mFragment;
     68     private final Context mContext;
     69     private final List<SearchResult> mSearchResults;
     70     private final List<SearchResult> mStaticallyRankedSearchResults;
     71     private Map<String, Set<? extends SearchResult>> mResultsMap;
     72     private final SearchFeatureProvider mSearchFeatureProvider;
     73     private List<Pair<String, Float>> mSearchRankingScores;
     74     private Handler mHandler;
     75     private boolean mSearchResultsLoaded;
     76     private boolean mSearchResultsUpdated;
     77 
     78     @IntDef({DISABLED, PENDING_RESULTS, SUCCEEDED, FAILED, TIMED_OUT})
     79     @Retention(RetentionPolicy.SOURCE)
     80     private @interface AsyncRankingState {}
     81     @VisibleForTesting
     82     static final int DISABLED = 0;
     83     @VisibleForTesting
     84     static final int PENDING_RESULTS = 1;
     85     @VisibleForTesting
     86     static final int SUCCEEDED = 2;
     87     @VisibleForTesting
     88     static final int FAILED = 3;
     89     @VisibleForTesting
     90     static final int TIMED_OUT = 4;
     91     private @AsyncRankingState int mAsyncRankingState;
     92 
     93     public SearchResultsAdapter(SearchFragment fragment,
     94             SearchFeatureProvider searchFeatureProvider) {
     95         mFragment = fragment;
     96         mContext = fragment.getContext().getApplicationContext();
     97         mSearchResults = new ArrayList<>();
     98         mResultsMap = new ArrayMap<>();
     99         mSearchRankingScores = new ArrayList<>();
    100         mStaticallyRankedSearchResults = new ArrayList<>();
    101         mSearchFeatureProvider = searchFeatureProvider;
    102 
    103         setHasStableIds(true);
    104     }
    105 
    106     @Override
    107     public SearchViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    108         final Context context = parent.getContext();
    109         final LayoutInflater inflater = LayoutInflater.from(context);
    110         final View view;
    111         switch (viewType) {
    112             case ResultPayload.PayloadType.INTENT:
    113                 view = inflater.inflate(R.layout.search_intent_item, parent, false);
    114                 return new IntentSearchViewHolder(view);
    115             case ResultPayload.PayloadType.INLINE_SWITCH:
    116                 // TODO (b/62807132) replace layout InlineSwitchViewHolder and return an
    117                 // InlineSwitchViewHolder.
    118                 view = inflater.inflate(R.layout.search_intent_item, parent, false);
    119                 return new IntentSearchViewHolder(view);
    120             case ResultPayload.PayloadType.INLINE_LIST:
    121                 // TODO (b/62807132) build a inline-list view holder & layout.
    122                 view = inflater.inflate(R.layout.search_intent_item, parent, false);
    123                 return new IntentSearchViewHolder(view);
    124             case ResultPayload.PayloadType.SAVED_QUERY:
    125                 view = inflater.inflate(R.layout.search_saved_query_item, parent, false);
    126                 return new SavedQueryViewHolder(view);
    127             default:
    128                 return null;
    129         }
    130     }
    131 
    132     @Override
    133     public void onBindViewHolder(SearchViewHolder holder, int position) {
    134         holder.onBind(mFragment, mSearchResults.get(position));
    135     }
    136 
    137     @Override
    138     public long getItemId(int position) {
    139         return mSearchResults.get(position).stableId;
    140     }
    141 
    142     @Override
    143     public int getItemViewType(int position) {
    144         return mSearchResults.get(position).viewType;
    145     }
    146 
    147     @Override
    148     public int getItemCount() {
    149         return mSearchResults.size();
    150     }
    151 
    152     @MainThread
    153     @Override
    154     public void onRankingScoresAvailable(List<Pair<String, Float>> searchRankingScores) {
    155         // Received the scores, stop the timeout timer.
    156         getHandler().removeMessages(MSG_RANKING_TIMED_OUT);
    157         if (mAsyncRankingState == PENDING_RESULTS) {
    158             mAsyncRankingState = SUCCEEDED;
    159             mSearchRankingScores.clear();
    160             mSearchRankingScores.addAll(searchRankingScores);
    161             if (canUpdateSearchResults()) {
    162                 updateSearchResults();
    163             }
    164         } else {
    165             Log.w(TAG, "Ranking scores became available in invalid state: " + mAsyncRankingState);
    166         }
    167     }
    168 
    169     @MainThread
    170     @Override
    171     public void onRankingFailed() {
    172         if (mAsyncRankingState == PENDING_RESULTS) {
    173             mAsyncRankingState = FAILED;
    174             if (canUpdateSearchResults()) {
    175                 updateSearchResults();
    176             }
    177         } else {
    178             Log.w(TAG, "Ranking scores failed in invalid states: " + mAsyncRankingState);
    179         }
    180     }
    181 
    182    /**
    183      * Store the results from each of the loaders to be merged when all loaders are finished.
    184      *
    185      * @param results         the results from the loader.
    186      * @param loaderClassName class name of the loader.
    187      */
    188     @MainThread
    189     public void addSearchResults(Set<? extends SearchResult> results, String loaderClassName) {
    190         if (results == null) {
    191             return;
    192         }
    193         mResultsMap.put(loaderClassName, results);
    194     }
    195 
    196     /**
    197      * Displays recent searched queries.
    198      *
    199      * @return The number of saved queries to display
    200      */
    201     public int displaySavedQuery(List<? extends SearchResult> data) {
    202         clearResults();
    203         mSearchResults.addAll(data);
    204         notifyDataSetChanged();
    205         return mSearchResults.size();
    206     }
    207 
    208     /**
    209      * Notifies the adapter that all the unsorted results are loaded and now the ladapter can
    210      * proceed with ranking the results.
    211      */
    212     @MainThread
    213     public void notifyResultsLoaded() {
    214         mSearchResultsLoaded = true;
    215         // static ranking is skipped only if asyc ranking is already succeeded.
    216         if (mAsyncRankingState != SUCCEEDED) {
    217             doStaticRanking();
    218         }
    219         if (canUpdateSearchResults()) {
    220             updateSearchResults();
    221         }
    222     }
    223 
    224     public void clearResults() {
    225         mSearchResults.clear();
    226         mStaticallyRankedSearchResults.clear();
    227         mResultsMap.clear();
    228         notifyDataSetChanged();
    229     }
    230 
    231     @VisibleForTesting
    232     public List<SearchResult> getSearchResults() {
    233         return mSearchResults;
    234     }
    235 
    236     @MainThread
    237     public void initializeSearch(String query) {
    238         clearResults();
    239         mSearchResultsLoaded = false;
    240         mSearchResultsUpdated = false;
    241         if (mSearchFeatureProvider.isSmartSearchRankingEnabled(mContext)) {
    242             mAsyncRankingState = PENDING_RESULTS;
    243             mSearchFeatureProvider.cancelPendingSearchQuery(mContext);
    244             final Handler handler = getHandler();
    245             final long timeoutMs = mSearchFeatureProvider.smartSearchRankingTimeoutMs(mContext);
    246             handler.sendMessageDelayed(
    247                     handler.obtainMessage(MSG_RANKING_TIMED_OUT), timeoutMs);
    248             mSearchFeatureProvider.querySearchResults(mContext, query, this);
    249         } else {
    250             mAsyncRankingState = DISABLED;
    251         }
    252     }
    253 
    254     @AsyncRankingState int getAsyncRankingState() {
    255         return mAsyncRankingState;
    256     }
    257 
    258     /**
    259      * Merge the results from each of the loaders into one list for the adapter.
    260      * Prioritizes results from the local database over installed apps.
    261      */
    262     private void doStaticRanking() {
    263         List<? extends SearchResult> databaseResults =
    264                 getSortedLoadedResults(DB_RESULTS_LOADER_KEY);
    265         List<? extends SearchResult> installedAppResults =
    266                 getSortedLoadedResults(APP_RESULTS_LOADER_KEY);
    267         List<? extends SearchResult> accessibilityResults =
    268                 getSortedLoadedResults(ACCESSIBILITY_LOADER_KEY);
    269         List<? extends SearchResult> inputDeviceResults =
    270                 getSortedLoadedResults(INPUT_DEVICE_LOADER_KEY);
    271 
    272         int dbSize = databaseResults.size();
    273         int appSize = installedAppResults.size();
    274         int a11ySize = accessibilityResults.size();
    275         int inputDeviceSize = inputDeviceResults.size();
    276         int dbIndex = 0;
    277         int appIndex = 0;
    278         int a11yIndex = 0;
    279         int inputDeviceIndex = 0;
    280         int rank = SearchResult.TOP_RANK;
    281 
    282         // TODO: We need a helper method to do k-way merge.
    283         mStaticallyRankedSearchResults.clear();
    284         while (rank <= SearchResult.BOTTOM_RANK) {
    285             while ((dbIndex < dbSize) && (databaseResults.get(dbIndex).rank == rank)) {
    286                 mStaticallyRankedSearchResults.add(databaseResults.get(dbIndex++));
    287             }
    288             while ((appIndex < appSize) && (installedAppResults.get(appIndex).rank == rank)) {
    289                 mStaticallyRankedSearchResults.add(installedAppResults.get(appIndex++));
    290             }
    291             while ((a11yIndex < a11ySize) && (accessibilityResults.get(a11yIndex).rank == rank)) {
    292                 mStaticallyRankedSearchResults.add(accessibilityResults.get(a11yIndex++));
    293             }
    294             while (inputDeviceIndex < inputDeviceSize
    295                     && inputDeviceResults.get(inputDeviceIndex).rank == rank) {
    296                 mStaticallyRankedSearchResults.add(inputDeviceResults.get(inputDeviceIndex++));
    297             }
    298             rank++;
    299         }
    300 
    301         while (dbIndex < dbSize) {
    302             mStaticallyRankedSearchResults.add(databaseResults.get(dbIndex++));
    303         }
    304         while (appIndex < appSize) {
    305             mStaticallyRankedSearchResults.add(installedAppResults.get(appIndex++));
    306         }
    307         while(a11yIndex < a11ySize) {
    308             mStaticallyRankedSearchResults.add(accessibilityResults.get(a11yIndex++));
    309         }
    310         while (inputDeviceIndex < inputDeviceSize) {
    311             mStaticallyRankedSearchResults.add(inputDeviceResults.get(inputDeviceIndex++));
    312         }
    313     }
    314 
    315     private void updateSearchResults() {
    316         switch (mAsyncRankingState) {
    317             case PENDING_RESULTS:
    318                 break;
    319             case DISABLED:
    320             case FAILED:
    321             case TIMED_OUT:
    322                 // When DISABLED or FAILED or TIMED_OUT, we use static ranking results.
    323                 postSearchResults(mStaticallyRankedSearchResults, false);
    324                 break;
    325             case SUCCEEDED:
    326                 postSearchResults(doAsyncRanking(), true);
    327                 break;
    328         }
    329     }
    330 
    331     private boolean canUpdateSearchResults() {
    332         // Results are not updated yet and db results are loaded and we are not waiting on async
    333         // ranking scores.
    334         return !mSearchResultsUpdated
    335                 && mSearchResultsLoaded
    336                 && mAsyncRankingState != PENDING_RESULTS;
    337     }
    338 
    339     @VisibleForTesting
    340     List<SearchResult> doAsyncRanking() {
    341         Set<? extends SearchResult> databaseResults =
    342                 getUnsortedLoadedResults(DB_RESULTS_LOADER_KEY);
    343         List<? extends SearchResult> installedAppResults =
    344                 getSortedLoadedResults(APP_RESULTS_LOADER_KEY);
    345         List<? extends SearchResult> accessibilityResults =
    346                 getSortedLoadedResults(ACCESSIBILITY_LOADER_KEY);
    347         List<? extends SearchResult> inputDeviceResults =
    348                 getSortedLoadedResults(INPUT_DEVICE_LOADER_KEY);
    349         int dbSize = databaseResults.size();
    350         int appSize = installedAppResults.size();
    351         int a11ySize = accessibilityResults.size();
    352         int inputDeviceSize = inputDeviceResults.size();
    353 
    354         final List<SearchResult> asyncRankingResults = new ArrayList<>(
    355                 dbSize + appSize + a11ySize + inputDeviceSize);
    356         TreeSet<SearchResult> dbResultsSortedByScores = new TreeSet<>(
    357                 new Comparator<SearchResult>() {
    358                     @Override
    359                     public int compare(SearchResult o1, SearchResult o2) {
    360                         float score1 = getRankingScoreByStableId(o1.stableId);
    361                         float score2 = getRankingScoreByStableId(o2.stableId);
    362                         if (score1 > score2) {
    363                             return -1;
    364                         } else if (score1 == score2) {
    365                             return 0;
    366                         } else {
    367                             return 1;
    368                         }
    369                     }
    370                 });
    371         dbResultsSortedByScores.addAll(databaseResults);
    372         asyncRankingResults.addAll(dbResultsSortedByScores);
    373         // Other results are not ranked by async ranking and appended at the end of the list.
    374         asyncRankingResults.addAll(installedAppResults);
    375         asyncRankingResults.addAll(accessibilityResults);
    376         asyncRankingResults.addAll(inputDeviceResults);
    377         return asyncRankingResults;
    378     }
    379 
    380     @VisibleForTesting
    381     Set<? extends SearchResult> getUnsortedLoadedResults(String loaderKey) {
    382         return mResultsMap.containsKey(loaderKey) ? mResultsMap.get(loaderKey) : new HashSet<>();
    383     }
    384 
    385     @VisibleForTesting
    386     List<? extends SearchResult> getSortedLoadedResults(String loaderKey) {
    387         List<? extends SearchResult> sortedLoadedResults =
    388                 new ArrayList<>(getUnsortedLoadedResults(loaderKey));
    389         Collections.sort(sortedLoadedResults);
    390         return sortedLoadedResults;
    391     }
    392 
    393     /**
    394      * Looks up ranking score for stableId
    395      * @param stableId String of stableId
    396      * @return the ranking score corresponding to the given stableId. If there is no score
    397      * available for this stableId, -Float.MAX_VALUE is returned.
    398      */
    399     @VisibleForTesting
    400     Float getRankingScoreByStableId(int stableId) {
    401         for (Pair<String, Float> rankingScore : mSearchRankingScores) {
    402             if (Integer.toString(stableId).compareTo(rankingScore.first) == 0) {
    403                 return rankingScore.second;
    404             }
    405         }
    406         // If stableId not found in the list, we assign the minimum score so it will appear at
    407         // the end of the list.
    408         Log.w(TAG, "stableId " + stableId + " was not in the ranking scores.");
    409         return -Float.MAX_VALUE;
    410     }
    411 
    412     @VisibleForTesting
    413     Handler getHandler() {
    414         if (mHandler == null) {
    415             mHandler = new Handler(Looper.getMainLooper()) {
    416                 @Override
    417                 public void handleMessage(Message msg) {
    418                     if (msg.what == MSG_RANKING_TIMED_OUT) {
    419                         mSearchFeatureProvider.cancelPendingSearchQuery(mContext);
    420                         if (mAsyncRankingState == PENDING_RESULTS) {
    421                             mAsyncRankingState = TIMED_OUT;
    422                             if (canUpdateSearchResults()) {
    423                                 updateSearchResults();
    424                             }
    425                         } else {
    426                             Log.w(TAG, "Ranking scores timed out in invalid state: " +
    427                                     mAsyncRankingState);
    428                         }
    429                     }
    430                 }
    431             };
    432         }
    433         return mHandler;
    434     }
    435 
    436     @VisibleForTesting
    437     public void postSearchResults(List<SearchResult> newSearchResults, boolean detectMoves) {
    438         final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
    439                 new SearchResultDiffCallback(mSearchResults, newSearchResults), detectMoves);
    440         mSearchResults.clear();
    441         mSearchResults.addAll(newSearchResults);
    442         diffResult.dispatchUpdatesTo(this);
    443         mFragment.onSearchResultsDisplayed(mSearchResults.size());
    444         mSearchResultsUpdated = true;
    445     }
    446 }
    447