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