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