CursorBackedSuggestionCursor.java revision 185bb2e3881452c084fde44d9bee657f65881b0e
1/* 2 * Copyright (C) 2009 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 17package com.android.quicksearchbox; 18 19import android.app.SearchManager; 20import android.content.ComponentName; 21import android.content.Context; 22import android.content.Intent; 23import android.database.Cursor; 24import android.graphics.Rect; 25import android.net.Uri; 26import android.os.Bundle; 27import android.util.Log; 28import android.view.KeyEvent; 29 30import java.net.URISyntaxException; 31 32public abstract class CursorBackedSuggestionCursor extends AbstractSourceSuggestionCursor { 33 34 public static final String SUGGEST_COLUMN_SECONDARY_INTENT = "suggestion_secondary_intent"; 35 public static final String TARGET_RECT_KEY = "target_rect"; 36 37 private static final boolean DBG = true; 38 protected static final String TAG = "QSB.CursorBackedSuggestionCursor"; 39 40 /** The suggestions, or {@code null} if the suggestions query failed. */ 41 protected final Cursor mCursor; 42 43 /** Column index of {@link SearchManager.SUGGEST_COLUMN_FORMAT} in @{link mCursor}. */ 44 private final int mFormatCol; 45 46 /** Column index of {@link SearchManager.SUGGEST_COLUMN_TEXT_1} in @{link mCursor}. */ 47 private final int mText1Col; 48 49 /** Column index of {@link SearchManager.SUGGEST_COLUMN_TEXT_2} in @{link mCursor}. */ 50 private final int mText2Col; 51 52 /** Column index of {@link SearchManager.SUGGEST_COLUMN_ICON_1} in @{link mCursor}. */ 53 private final int mIcon1Col; 54 55 /** Column index of {@link SearchManager.SUGGEST_COLUMN_ICON_1} in @{link mCursor}. */ 56 private final int mIcon2Col; 57 58 /** True if this result has been closed. */ 59 private boolean mClosed = false; 60 61 public CursorBackedSuggestionCursor(String userQuery, Cursor cursor) { 62 super(userQuery); 63 mCursor = cursor; 64 mFormatCol = getColumnIndex(SearchManager.SUGGEST_COLUMN_FORMAT); 65 mText1Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1); 66 mText2Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2); 67 mIcon1Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1); 68 mIcon2Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2); 69 } 70 71 protected String getDefaultIntentAction() { 72 return getSource().getDefaultIntentAction(); 73 } 74 75 protected String getDefaultIntentData() { 76 return getSource().getDefaultIntentData(); 77 } 78 79 protected boolean shouldRewriteQueryFromData() { 80 return getSource().shouldRewriteQueryFromData(); 81 } 82 83 protected boolean shouldRewriteQueryFromText() { 84 return getSource().shouldRewriteQueryFromText(); 85 } 86 87 public boolean isFailed() { 88 return mCursor == null; 89 } 90 91 public void close() { 92 if (DBG) Log.d(TAG, "close()"); 93 if (mClosed) { 94 throw new IllegalStateException("Double close()"); 95 } 96 mClosed = true; 97 if (mCursor != null) { 98 // TODO: all operations on cross-process cursors can throw random exceptions 99 mCursor.close(); 100 } 101 } 102 103 @Override 104 protected void finalize() { 105 if (!mClosed) { 106 Log.e(TAG, "LEAK! Finalized without being closed: " + toString()); 107 close(); 108 } 109 } 110 111 public int getCount() { 112 if (mClosed) { 113 throw new IllegalStateException("getCount() after close()"); 114 } 115 if (mCursor == null) return 0; 116 // TODO: all operations on cross-process cursors can throw random exceptions 117 return mCursor.getCount(); 118 } 119 120 public void moveTo(int pos) { 121 if (mClosed) { 122 throw new IllegalStateException("moveTo(" + pos + ") after close()"); 123 } 124 // TODO: all operations on cross-process cursors can throw random exceptions 125 if (mCursor == null || pos < 0 || pos >= mCursor.getCount()) { 126 throw new IndexOutOfBoundsException(pos + ", count=" + getCount()); 127 } 128 // TODO: all operations on cross-process cursors can throw random exceptions 129 mCursor.moveToPosition(pos); 130 } 131 132 public int getPosition() { 133 if (mClosed) { 134 throw new IllegalStateException("getPosition after close()"); 135 } 136 return mCursor.getPosition(); 137 } 138 139 public String getSuggestionDisplayQuery() { 140 String query = getSuggestionQuery(); 141 if (query != null) { 142 return query; 143 } 144 if (shouldRewriteQueryFromData()) { 145 String data = getSuggestionIntentDataString(); 146 if (data != null) { 147 return data; 148 } 149 } 150 if (shouldRewriteQueryFromText()) { 151 String text1 = getSuggestionText1(); 152 if (text1 != null) { 153 return text1; 154 } 155 } 156 return null; 157 } 158 159 public String getShortcutId() { 160 return getStringOrNull(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID); 161 } 162 163 public String getSuggestionFormat() { 164 return getStringOrNull(mFormatCol); 165 } 166 167 public String getSuggestionText1() { 168 return getStringOrNull(mText1Col); 169 } 170 171 public String getSuggestionText2() { 172 return getStringOrNull(mText2Col); 173 } 174 175 public String getSuggestionIcon1() { 176 return getStringOrNull(mIcon1Col); 177 } 178 179 public String getSuggestionIcon2() { 180 return getStringOrNull(mIcon2Col); 181 } 182 183 public Intent getSuggestionIntent(Context context, Bundle appSearchData, 184 int actionKey, String actionMsg) { 185 String action = getSuggestionIntentAction(); 186 Uri data = getSuggestionIntentData(); 187 String query = getSuggestionQuery(); 188 String userQuery = getUserQuery(); 189 String extraData = getSuggestionIntentExtraData(); 190 191 // Now build the Intent 192 Intent intent = new Intent(action); 193 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 194 // We need CLEAR_TOP to avoid reusing an old task that has other activities 195 // on top of the one we want. 196 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 197 if (data != null) { 198 intent.setData(data); 199 } 200 intent.putExtra(SearchManager.USER_QUERY, userQuery); 201 if (query != null) { 202 intent.putExtra(SearchManager.QUERY, query); 203 } 204 if (extraData != null) { 205 intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData); 206 } 207 if (appSearchData != null) { 208 intent.putExtra(SearchManager.APP_DATA, appSearchData); 209 } 210 if (actionKey != KeyEvent.KEYCODE_UNKNOWN) { 211 intent.putExtra(SearchManager.ACTION_KEY, actionKey); 212 intent.putExtra(SearchManager.ACTION_MSG, actionMsg); 213 } 214 // TODO: Use this to tell sources this comes form global search 215 // The constants are currently hidden. 216 // intent.putExtra(SearchManager.SEARCH_MODE, 217 // SearchManager.MODE_GLOBAL_SEARCH_SUGGESTION); 218 intent.setComponent(getSuggestionIntentComponent(context, intent)); 219 return intent; 220 } 221 222 public Intent getSecondarySuggestionIntent(Context context, Bundle appSearchData, Rect target) { 223 String intentString = getStringOrNull(SUGGEST_COLUMN_SECONDARY_INTENT); 224 if (intentString != null) { 225 try { 226 Intent intent = Intent.parseUri(intentString, Intent.URI_INTENT_SCHEME); 227 if (appSearchData != null) { 228 intent.putExtra(SearchManager.APP_DATA, appSearchData); 229 } 230 // TODO: Do we need to pass action keys? 231 // TODO: Should we try to use defaults such as getDefaultIntentData? 232 intent.putExtra(TARGET_RECT_KEY, target); 233 intent.setComponent(getSuggestionIntentComponent(context, intent)); 234 return intent; 235 } catch (URISyntaxException e) { 236 Log.w(TAG, "Unable to parse secondary intent " + intentString); 237 } 238 } 239 return null; 240 } 241 242 /** 243 * Updates the intent with the component to which intents created 244 * from the current suggestion should be sent. 245 */ 246 protected ComponentName getSuggestionIntentComponent(Context context, Intent intent) { 247 ComponentName component = getSourceComponentName(); 248 // Limit intent resolution to the source package. 249 intent.setPackage(component.getPackageName()); 250 ComponentName resolvedComponent = intent.resolveActivity(context.getPackageManager()); 251 if (resolvedComponent != null) { 252 // It's ok if the intent resolves to an activity in the same 253 // package as component. We set the component explicitly to 254 // avoid having to re-resolve, and to prevent race conditions. 255 return resolvedComponent; 256 } else { 257 return component; 258 } 259 } 260 261 public boolean hasSecondaryIntent() { 262 return getStringOrNull(SUGGEST_COLUMN_SECONDARY_INTENT) != null; 263 } 264 265 /** 266 * Gets the intent action for the current suggestion. 267 */ 268 protected String getSuggestionIntentAction() { 269 // use specific action if supplied, or default action if supplied, or fixed default 270 String action = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_ACTION); 271 if (action == null) { 272 action = getDefaultIntentAction(); 273 if (action == null) { 274 action = Intent.ACTION_SEARCH; 275 } 276 } 277 return action; 278 } 279 280 /** 281 * Gets the query for the current suggestion. 282 */ 283 protected String getSuggestionQuery() { 284 return getStringOrNull(SearchManager.SUGGEST_COLUMN_QUERY); 285 } 286 287 private String getSuggestionIntentDataString() { 288 // use specific data if supplied, or default data if supplied 289 String data = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_DATA); 290 if (data == null) { 291 data = getDefaultIntentData(); 292 } 293 // then, if an ID was provided, append it. 294 if (data != null) { 295 String id = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); 296 if (id != null) { 297 data = data + "/" + Uri.encode(id); 298 } 299 } 300 return data; 301 } 302 303 /** 304 * Gets the intent data for the current suggestion. 305 */ 306 protected Uri getSuggestionIntentData() { 307 String data = getSuggestionIntentDataString(); 308 return (data == null) ? null : Uri.parse(data); 309 } 310 311 /** 312 * Gets the intent extra data for the current suggestion. 313 */ 314 protected String getSuggestionIntentExtraData() { 315 return getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); 316 } 317 318 /** 319 * Gets the index of a column in {@link mCursor} by name. 320 * 321 * @return The index, or {@code -1} if the column was not found. 322 */ 323 protected int getColumnIndex(String colName) { 324 if (mCursor == null) return -1; 325 // TODO: all operations on cross-process cursors can throw random exceptions 326 return mCursor.getColumnIndex(colName); 327 } 328 329 /** 330 * Gets the string value of a column in {@link mCursor} by column index. 331 * 332 * @param col Column index. 333 * @return The string value, or {@code null}. 334 */ 335 protected String getStringOrNull(int col) { 336 if (mCursor == null) return null; 337 if (col == -1) { 338 return null; 339 } 340 try { 341 // TODO: all operations on cross-process cursors can throw random exceptions 342 return mCursor.getString(col); 343 } catch (Exception e) { 344 Log.e(TAG, 345 "unexpected error retrieving valid column from cursor, " 346 + "did the remote process die?", e); 347 return null; 348 } 349 } 350 351 /** 352 * Gets the string value of a column in {@link mCursor} by column name. 353 * 354 * @param colName Column name. 355 * @return The string value, or {@code null}. 356 */ 357 protected String getStringOrNull(String colName) { 358 int col = getColumnIndex(colName); 359 return getStringOrNull(col); 360 } 361 362 private String makeKeyComponent(String str) { 363 return str == null ? "" : str; 364 } 365 366 public String getSuggestionKey() { 367 String action = makeKeyComponent(getSuggestionIntentAction()); 368 String data = makeKeyComponent(getSuggestionIntentDataString()); 369 String query = makeKeyComponent(getSuggestionQuery()); 370 // calculating accurate size of string builder avoids an allocation vs starting with 371 // the default size and having to expand. 372 int size = action.length() + 2 + data.length() + query.length(); 373 return new StringBuilder(size) 374 .append(action) 375 .append('#') 376 .append(data) 377 .append('#') 378 .append(query) 379 .toString(); 380 } 381} 382