• 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.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 
SearchResultsAdapter(SearchFragment fragment, SearchFeatureProvider searchFeatureProvider)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
onCreateViewHolder(ViewGroup parent, int viewType)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
onBindViewHolder(SearchViewHolder holder, int position)133     public void onBindViewHolder(SearchViewHolder holder, int position) {
134         holder.onBind(mFragment, mSearchResults.get(position));
135     }
136 
137     @Override
getItemId(int position)138     public long getItemId(int position) {
139         return mSearchResults.get(position).stableId;
140     }
141 
142     @Override
getItemViewType(int position)143     public int getItemViewType(int position) {
144         return mSearchResults.get(position).viewType;
145     }
146 
147     @Override
getItemCount()148     public int getItemCount() {
149         return mSearchResults.size();
150     }
151 
152     @MainThread
153     @Override
onRankingScoresAvailable(List<Pair<String, Float>> searchRankingScores)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
onRankingFailed()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
addSearchResults(Set<? extends SearchResult> results, String loaderClassName)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      */
displaySavedQuery(List<? extends SearchResult> data)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
notifyResultsLoaded()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 
clearResults()224     public void clearResults() {
225         mSearchResults.clear();
226         mStaticallyRankedSearchResults.clear();
227         mResultsMap.clear();
228         notifyDataSetChanged();
229     }
230 
231     @VisibleForTesting
getSearchResults()232     public List<SearchResult> getSearchResults() {
233         return mSearchResults;
234     }
235 
236     @MainThread
initializeSearch(String query)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 
getAsyncRankingState()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      */
doStaticRanking()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 
updateSearchResults()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 
canUpdateSearchResults()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
doAsyncRanking()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
getUnsortedLoadedResults(String loaderKey)381     Set<? extends SearchResult> getUnsortedLoadedResults(String loaderKey) {
382         return mResultsMap.containsKey(loaderKey) ? mResultsMap.get(loaderKey) : new HashSet<>();
383     }
384 
385     @VisibleForTesting
getSortedLoadedResults(String loaderKey)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
getRankingScoreByStableId(int stableId)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
getHandler()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
postSearchResults(List<SearchResult> newSearchResults, boolean detectMoves)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