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 
18 package com.android.settings.search2;
19 
20 import android.content.ComponentName;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.pm.PackageManager;
24 import android.content.res.Resources;
25 import android.database.Cursor;
26 import android.graphics.drawable.Drawable;
27 import android.os.BadParcelableException;
28 import android.os.Bundle;
29 import android.text.TextUtils;
30 import android.util.Log;
31 
32 import com.android.internal.logging.nano.MetricsProto;
33 import com.android.settings.SettingsActivity;
34 import com.android.settings.Utils;
35 import com.android.settings.dashboard.SiteMapManager;
36 
37 import java.util.ArrayList;
38 import java.util.Arrays;
39 import java.util.Collections;
40 import java.util.HashMap;
41 import java.util.HashSet;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.Set;
45 
46 import static com.android.settings.search2.DatabaseResultLoader.COLUMN_INDEX_CLASS_NAME;
47 import static com.android.settings.search2.DatabaseResultLoader.COLUMN_INDEX_ICON;
48 import static com.android.settings.search2.DatabaseResultLoader.COLUMN_INDEX_ID;
49 import static com.android.settings.search2.DatabaseResultLoader.COLUMN_INDEX_INTENT_ACTION;
50 import static com.android.settings.search2.DatabaseResultLoader
51         .COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS;
52 import static com.android.settings.search2.DatabaseResultLoader
53         .COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE;
54 import static com.android.settings.search2.DatabaseResultLoader.COLUMN_INDEX_KEY;
55 import static com.android.settings.search2.DatabaseResultLoader.COLUMN_INDEX_PAYLOAD;
56 import static com.android.settings.search2.DatabaseResultLoader.COLUMN_INDEX_PAYLOAD_TYPE;
57 import static com.android.settings.search2.DatabaseResultLoader.COLUMN_INDEX_SCREEN_TITLE;
58 import static com.android.settings.search2.DatabaseResultLoader.COLUMN_INDEX_SUMMARY_ON;
59 import static com.android.settings.search2.DatabaseResultLoader.COLUMN_INDEX_TITLE;
60 
61 /**
62  * Controller to Build search results from {@link Cursor} Objects.
63  *
64  * Each converted {@link Cursor} has the following fields:
65  * - String Title
66  * - String Summary
67  * - int rank
68  * - {@link Drawable} icon
69  * - {@link ResultPayload} payload
70  */
71 class CursorToSearchResultConverter {
72 
73     private final String TAG = "CursorConverter";
74 
75     private final String mQueryText;
76 
77     private final Context mContext;
78 
79     private final Set<String> mKeys;
80 
81     private final int LONG_TITLE_LENGTH = 20;
82 
83     private static final String[] whiteList = {
84             "main_toggle_wifi",
85             "main_toggle_bluetooth",
86             "toggle_airplane",
87             "tether_settings",
88             "battery_saver",
89             "toggle_nfc",
90             "restrict_background",
91             "data_usage_enable",
92             "button_roaming_key",
93     };
94     private static final Set<String> prioritySettings = new HashSet(Arrays.asList(whiteList));
95 
96 
CursorToSearchResultConverter(Context context, String queryText)97     public CursorToSearchResultConverter(Context context, String queryText) {
98         mContext = context;
99         mKeys = new HashSet<>();
100         mQueryText = queryText;
101     }
102 
convertCursor(SiteMapManager sitemapManager, Cursor cursorResults, int baseRank)103     public List<SearchResult> convertCursor(SiteMapManager sitemapManager,
104             Cursor cursorResults, int baseRank) {
105         if (cursorResults == null) {
106             return null;
107         }
108         final Map<String, Context> contextMap = new HashMap<>();
109         final List<SearchResult> results = new ArrayList<>();
110 
111         while (cursorResults.moveToNext()) {
112             SearchResult result = buildSingleSearchResultFromCursor(sitemapManager,
113                     contextMap, cursorResults, baseRank);
114             if (result != null) {
115                 results.add(result);
116             }
117         }
118         Collections.sort(results);
119         return results;
120     }
121 
buildSingleSearchResultFromCursor(SiteMapManager sitemapManager, Map<String, Context> contextMap, Cursor cursor, int baseRank)122     private SearchResult buildSingleSearchResultFromCursor(SiteMapManager sitemapManager,
123             Map<String, Context> contextMap, Cursor cursor, int baseRank) {
124         final String docId = cursor.getString(COLUMN_INDEX_ID);
125         /* Make sure that this result has not yet been added as a result. Checking the docID
126            covers the case of multiple queries matching the same row, but we need to also to check
127            for potentially the same named or slightly varied names pointing to the same page.
128          */
129         if (mKeys.contains(docId)) {
130             return null;
131         }
132         mKeys.add(docId);
133 
134         final String pkgName = cursor.getString(COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE);
135         final String action = cursor.getString(COLUMN_INDEX_INTENT_ACTION);
136         final String title = cursor.getString(COLUMN_INDEX_TITLE);
137         final String summaryOn = cursor.getString(COLUMN_INDEX_SUMMARY_ON);
138         final String className = cursor.getString(COLUMN_INDEX_CLASS_NAME);
139         final String key = cursor.getString(COLUMN_INDEX_KEY);
140         final String iconResStr = cursor.getString(COLUMN_INDEX_ICON);
141         final int payloadType = cursor.getInt(COLUMN_INDEX_PAYLOAD_TYPE);
142         final byte[] marshalledPayload = cursor.getBlob(COLUMN_INDEX_PAYLOAD);
143         final ResultPayload payload;
144 
145         if (marshalledPayload != null) {
146             payload = getUnmarshalledPayload(marshalledPayload, payloadType);
147         } else if (payloadType == ResultPayload.PayloadType.INTENT) {
148             payload = getIntentPayload(cursor, action, key, className, pkgName);
149         } else {
150             Log.w(TAG, "Error creating payload - bad marshalling data or mismatched types");
151             return null;
152         }
153 
154         final List<String> breadcrumbs = getBreadcrumbs(sitemapManager, cursor);
155         final int rank = getRank(title, breadcrumbs, baseRank, key);
156 
157         final SearchResult.Builder builder = new SearchResult.Builder();
158         builder.addTitle(title)
159                 .addSummary(summaryOn)
160                 .addBreadcrumbs(breadcrumbs)
161                 .addRank(rank)
162                 .addIcon(getIconForPackage(contextMap, pkgName, className, iconResStr))
163                 .addPayload(payload);
164         return builder.build();
165     }
166 
getIconForPackage(Map<String, Context> contextMap, String pkgName, String className, String iconResStr)167     private Drawable getIconForPackage(Map<String, Context> contextMap, String pkgName,
168             String className, String iconResStr) {
169         final int iconId = TextUtils.isEmpty(iconResStr)
170                 ? 0 : Integer.parseInt(iconResStr);
171         Drawable icon;
172         Context packageContext;
173         if (iconId == 0) {
174             icon = null;
175         } else {
176             if (TextUtils.isEmpty(className) && !TextUtils.isEmpty(pkgName)) {
177                 packageContext = contextMap.get(pkgName);
178                 if (packageContext == null) {
179                     try {
180                         packageContext = mContext.createPackageContext(pkgName, 0);
181                     } catch (PackageManager.NameNotFoundException e) {
182                         Log.e(TAG, "Cannot create Context for package: " + pkgName);
183                         return null;
184                     }
185                     contextMap.put(pkgName, packageContext);
186                 }
187             } else {
188                 packageContext = mContext;
189             }
190             try {
191                 icon = packageContext.getDrawable(iconId);
192             } catch (Resources.NotFoundException nfe) {
193                 icon = null;
194             }
195         }
196         return icon;
197     }
198 
getIntentPayload(Cursor cursor, String action, String key, String className, String pkgName )199     private IntentPayload getIntentPayload(Cursor cursor, String action, String key,
200             String className, String pkgName ) {
201         IntentPayload payload;
202         if (TextUtils.isEmpty(action)) {
203             final String screenTitle = cursor.getString(COLUMN_INDEX_SCREEN_TITLE);
204             // Action is null, we will launch it as a sub-setting
205             final Bundle args = new Bundle();
206             args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key);
207             final Intent intent = Utils.onBuildStartFragmentIntent(mContext,
208                     className, args, null, 0, screenTitle, false,
209                     MetricsProto.MetricsEvent.DASHBOARD_SEARCH_RESULTS);
210             payload = new IntentPayload(intent);
211         } else {
212             final Intent intent = new Intent(action);
213             final String targetClass = cursor.getString(COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS);
214             if (!TextUtils.isEmpty(pkgName) && !TextUtils.isEmpty(targetClass)) {
215                 final ComponentName component = new ComponentName(pkgName, targetClass);
216                 intent.setComponent(component);
217             }
218             intent.putExtra(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key);
219             payload = new IntentPayload(intent);
220         }
221         return payload;
222     }
223 
getUnmarshalledPayload(byte[] unmarshalledPayload, int payloadType)224     private ResultPayload getUnmarshalledPayload(byte[] unmarshalledPayload, int payloadType) {
225         try {
226             switch (payloadType) {
227                 case ResultPayload.PayloadType.INLINE_SWITCH:
228                     return ResultPayloadUtils.unmarshall(unmarshalledPayload,
229                             InlineSwitchPayload.CREATOR);
230             }
231         } catch (BadParcelableException e) {
232             Log.w(TAG, "Error creating parcelable: " + e);
233         }
234         return null;
235     }
236 
getBreadcrumbs(SiteMapManager siteMapManager, Cursor cursor)237     private List<String> getBreadcrumbs(SiteMapManager siteMapManager, Cursor cursor) {
238         final String screenTitle = cursor.getString(COLUMN_INDEX_SCREEN_TITLE);
239         final String screenClass = cursor.getString(COLUMN_INDEX_CLASS_NAME);
240         return siteMapManager == null ? null : siteMapManager.buildBreadCrumb(mContext, screenClass,
241                 screenTitle);
242     }
243 
244     /** Uses the breadcrumbs to determine the offset to the base rank.
245      *  There are three checks
246      *  A) If the result is prioritized and the highest base level
247      *  B) If the query matches the highest level menu title
248      *  C) If the query matches a subsequent menu title
249      *  D) Is the title longer than 20
250      *
251      *  If the query matches A, set it to TOP_RANK
252      *  If the query matches B and C, the offset is 0.
253      *  If the query matches C only, the offset is 1.
254      *  If the query matches neither B nor C, the offset is 2.
255      *  If the query matches D, the offset is 2
256 
257      * @param title of the result.
258      * @param crumbs from the Information Architecture
259      * @param baseRank of the result. Lower if it's a better result.
260      * @return
261      */
getRank(String title, List<String> crumbs, int baseRank, String key)262     private int getRank(String title, List<String> crumbs, int baseRank, String key) {
263         // The result can only be prioritized if it is a top ranked result.
264         if (prioritySettings.contains(key) && baseRank < DatabaseResultLoader.BASE_RANKS[1]) {
265             return SearchResult.TOP_RANK;
266         }
267         if (title.length() > LONG_TITLE_LENGTH) {
268             return baseRank + 2;
269         }
270         return baseRank;
271     }
272 
273 }
274