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.Intent;
22import android.database.Cursor;
23import android.database.DataSetObserver;
24import android.net.Uri;
25import android.util.Log;
26
27public abstract class CursorBackedSuggestionCursor implements SuggestionCursor {
28
29    private static final boolean DBG = false;
30    protected static final String TAG = "QSB.CursorBackedSuggestionCursor";
31
32    public static final String SUGGEST_COLUMN_LOG_TYPE = "suggest_log_type";
33
34    private final String mUserQuery;
35
36    /** The suggestions, or {@code null} if the suggestions query failed. */
37    protected final Cursor mCursor;
38
39    /** Column index of {@link SearchManager#SUGGEST_COLUMN_FORMAT} in @{link mCursor}. */
40    private final int mFormatCol;
41
42    /** Column index of {@link SearchManager#SUGGEST_COLUMN_TEXT_1} in @{link mCursor}. */
43    private final int mText1Col;
44
45    /** Column index of {@link SearchManager#SUGGEST_COLUMN_TEXT_2} in @{link mCursor}. */
46    private final int mText2Col;
47
48    /** Column index of {@link SearchManager#SUGGEST_COLUMN_TEXT_2_URL} in @{link mCursor}. */
49    private final int mText2UrlCol;
50
51    /** Column index of {@link SearchManager#SUGGEST_COLUMN_ICON_1} in @{link mCursor}. */
52    private final int mIcon1Col;
53
54    /** Column index of {@link SearchManager#SUGGEST_COLUMN_ICON_1} in @{link mCursor}. */
55    private final int mIcon2Col;
56
57    /** Column index of {@link SearchManager#SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING}
58     * in @{link mCursor}.
59     **/
60    private final int mRefreshSpinnerCol;
61
62    /** True if this result has been closed. */
63    private boolean mClosed = false;
64
65    public CursorBackedSuggestionCursor(String userQuery, Cursor cursor) {
66        mUserQuery = userQuery;
67        mCursor = cursor;
68        mFormatCol = getColumnIndex(SearchManager.SUGGEST_COLUMN_FORMAT);
69        mText1Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1);
70        mText2Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2);
71        mText2UrlCol = getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL);
72        mIcon1Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1);
73        mIcon2Col = getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2);
74        mRefreshSpinnerCol = getColumnIndex(SearchManager.SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING);
75    }
76
77    public String getUserQuery() {
78        return mUserQuery;
79    }
80
81    public abstract Source getSuggestionSource();
82
83    public String getSuggestionLogType() {
84        return getStringOrNull(SUGGEST_COLUMN_LOG_TYPE);
85    }
86
87    public void close() {
88        if (DBG) Log.d(TAG, "close()");
89        if (mClosed) {
90            throw new IllegalStateException("Double close()");
91        }
92        mClosed = true;
93        if (mCursor != null) {
94            try {
95                mCursor.close();
96            } catch (RuntimeException ex) {
97                // all operations on cross-process cursors can throw random exceptions
98                Log.e(TAG, "close() failed, ", ex);
99            }
100        }
101    }
102
103    @Override
104    protected void finalize() {
105        if (!mClosed) {
106            Log.e(TAG, "LEAK! Finalized without being closed: " + toString());
107        }
108    }
109
110    public int getCount() {
111        if (mClosed) {
112            throw new IllegalStateException("getCount() after close()");
113        }
114        if (mCursor == null) return 0;
115        try {
116            return mCursor.getCount();
117        } catch (RuntimeException ex) {
118            // all operations on cross-process cursors can throw random exceptions
119            Log.e(TAG, "getCount() failed, ", ex);
120            return 0;
121        }
122    }
123
124    public void moveTo(int pos) {
125        if (mClosed) {
126            throw new IllegalStateException("moveTo(" + pos + ") after close()");
127        }
128        try {
129            if (!mCursor.moveToPosition(pos)) {
130                Log.e(TAG, "moveToPosition(" + pos + ") failed, count=" + getCount());
131            }
132        } catch (RuntimeException ex) {
133            // all operations on cross-process cursors can throw random exceptions
134            Log.e(TAG, "moveToPosition() failed, ", ex);
135        }
136    }
137
138    public boolean moveToNext() {
139        if (mClosed) {
140            throw new IllegalStateException("moveToNext() after close()");
141        }
142        try {
143            return mCursor.moveToNext();
144        } catch (RuntimeException ex) {
145            // all operations on cross-process cursors can throw random exceptions
146            Log.e(TAG, "moveToNext() failed, ", ex);
147            return false;
148        }
149    }
150
151    public int getPosition() {
152        if (mClosed) {
153            throw new IllegalStateException("getPosition after close()");
154        }
155        try {
156            return mCursor.getPosition();
157        } catch (RuntimeException ex) {
158            // all operations on cross-process cursors can throw random exceptions
159            Log.e(TAG, "getPosition() failed, ", ex);
160            return -1;
161        }
162    }
163
164    public String getShortcutId() {
165        return getStringOrNull(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
166    }
167
168    public String getSuggestionFormat() {
169        return getStringOrNull(mFormatCol);
170    }
171
172    public String getSuggestionText1() {
173        return getStringOrNull(mText1Col);
174    }
175
176    public String getSuggestionText2() {
177        return getStringOrNull(mText2Col);
178    }
179
180    public String getSuggestionText2Url() {
181        return getStringOrNull(mText2UrlCol);
182    }
183
184    public String getSuggestionIcon1() {
185        return getStringOrNull(mIcon1Col);
186    }
187
188    public String getSuggestionIcon2() {
189        return getStringOrNull(mIcon2Col);
190    }
191
192    public boolean isSpinnerWhileRefreshing() {
193        return "true".equals(getStringOrNull(mRefreshSpinnerCol));
194    }
195
196    /**
197     * Gets the intent action for the current suggestion.
198     */
199    public String getSuggestionIntentAction() {
200        String action = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
201        if (action != null) return action;
202        return getSuggestionSource().getDefaultIntentAction();
203    }
204
205    public abstract ComponentName getSuggestionIntentComponent();
206
207    /**
208     * Gets the query for the current suggestion.
209     */
210    public String getSuggestionQuery() {
211        return getStringOrNull(SearchManager.SUGGEST_COLUMN_QUERY);
212    }
213
214    public String getSuggestionIntentDataString() {
215         // use specific data if supplied, or default data if supplied
216         String data = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_DATA);
217         if (data == null) {
218             data = getSuggestionSource().getDefaultIntentData();
219         }
220         // then, if an ID was provided, append it.
221         if (data != null) {
222             String id = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
223             if (id != null) {
224                 data = data + "/" + Uri.encode(id);
225             }
226         }
227         return data;
228     }
229
230    /**
231     * Gets the intent extra data for the current suggestion.
232     */
233    public String getSuggestionIntentExtraData() {
234        return getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
235    }
236
237    public boolean isWebSearchSuggestion() {
238        return Intent.ACTION_WEB_SEARCH.equals(getSuggestionIntentAction());
239    }
240
241    /**
242     * Gets the index of a column in {@link #mCursor} by name.
243     *
244     * @return The index, or {@code -1} if the column was not found.
245     */
246    protected int getColumnIndex(String colName) {
247        if (mCursor == null) return -1;
248        try {
249            return mCursor.getColumnIndex(colName);
250        } catch (RuntimeException ex) {
251            // all operations on cross-process cursors can throw random exceptions
252            Log.e(TAG, "getColumnIndex() failed, ", ex);
253            return -1;
254        }
255    }
256
257    /**
258     * Gets the string value of a column in {@link #mCursor} by column index.
259     *
260     * @param col Column index.
261     * @return The string value, or {@code null}.
262     */
263    protected String getStringOrNull(int col) {
264        if (mCursor == null) return null;
265        if (col == -1) {
266            return null;
267        }
268        try {
269            return mCursor.getString(col);
270        } catch (RuntimeException ex) {
271            // all operations on cross-process cursors can throw random exceptions
272            Log.e(TAG, "getString() failed, ", ex);
273            return null;
274        }
275    }
276
277    /**
278     * Gets the string value of a column in {@link #mCursor} by column name.
279     *
280     * @param colName Column name.
281     * @return The string value, or {@code null}.
282     */
283    protected String getStringOrNull(String colName) {
284        int col = getColumnIndex(colName);
285        return getStringOrNull(col);
286    }
287
288    public void registerDataSetObserver(DataSetObserver observer) {
289        // We don't watch Cursor-backed SuggestionCursors for changes
290    }
291
292    public void unregisterDataSetObserver(DataSetObserver observer) {
293        // We don't watch Cursor-backed SuggestionCursors for changes
294    }
295
296    @Override
297    public String toString() {
298        return getClass().getSimpleName() + "[" + mUserQuery + "]";
299    }
300
301}
302