1/*
2 * Copyright (C) 2008 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 android.provider;
18
19import android.content.ContentResolver;
20import android.content.ContentValues;
21import android.content.Context;
22import android.content.SearchRecentSuggestionsProvider;
23import android.database.Cursor;
24import android.net.Uri;
25import android.text.TextUtils;
26import android.util.Log;
27
28/**
29 * This is a utility class providing access to
30 * {@link android.content.SearchRecentSuggestionsProvider}.
31 *
32 * <p>Unlike some utility classes, this one must be instantiated and properly initialized, so that
33 * it can be configured to operate with the search suggestions provider that you have created.
34 *
35 * <p>Typically, you will do this in your searchable activity, each time you receive an incoming
36 * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH} Intent.  The code to record each
37 * incoming query is as follows:
38 * <pre class="prettyprint">
39 *      SearchSuggestions suggestions = new SearchSuggestions(this,
40 *              MySuggestionsProvider.AUTHORITY, MySuggestionsProvider.MODE);
41 *      suggestions.saveRecentQuery(queryString, null);
42 * </pre>
43 *
44 * <p>For a working example, see SearchSuggestionSampleProvider and SearchQueryResults in
45 * samples/ApiDemos/app.
46 */
47public class SearchRecentSuggestions {
48    // debugging support
49    private static final String LOG_TAG = "SearchSuggestions";
50    // DELETE ME (eventually)
51    private static final int DBG_SUGGESTION_TIMESTAMPS = 0;
52
53    // This is a superset of all possible column names (need not all be in table)
54    private static class SuggestionColumns implements BaseColumns {
55        public static final String DISPLAY1 = "display1";
56        public static final String DISPLAY2 = "display2";
57        public static final String QUERY = "query";
58        public static final String DATE = "date";
59    }
60
61    /* if you change column order you must also change indices below */
62    /**
63     * This is the database projection that can be used to view saved queries, when
64     * configured for one-line operation.
65     */
66    public static final String[] QUERIES_PROJECTION_1LINE = new String[] {
67        SuggestionColumns._ID,
68        SuggestionColumns.DATE,
69        SuggestionColumns.QUERY,
70        SuggestionColumns.DISPLAY1,
71    };
72    /* if you change column order you must also change indices below */
73    /**
74     * This is the database projection that can be used to view saved queries, when
75     * configured for two-line operation.
76     */
77    public static final String[] QUERIES_PROJECTION_2LINE = new String[] {
78        SuggestionColumns._ID,
79        SuggestionColumns.DATE,
80        SuggestionColumns.QUERY,
81        SuggestionColumns.DISPLAY1,
82        SuggestionColumns.DISPLAY2,
83    };
84
85    /* these indices depend on QUERIES_PROJECTION_xxx */
86    /** Index into the provided query projections.  For use with Cursor.update methods. */
87    public static final int QUERIES_PROJECTION_DATE_INDEX = 1;
88    /** Index into the provided query projections.  For use with Cursor.update methods. */
89    public static final int QUERIES_PROJECTION_QUERY_INDEX = 2;
90    /** Index into the provided query projections.  For use with Cursor.update methods. */
91    public static final int QUERIES_PROJECTION_DISPLAY1_INDEX = 3;
92    /** Index into the provided query projections.  For use with Cursor.update methods. */
93    public static final int QUERIES_PROJECTION_DISPLAY2_INDEX = 4;  // only when 2line active
94
95    /* columns needed to determine whether to truncate history */
96    private static final String[] TRUNCATE_HISTORY_PROJECTION = new String[] {
97        SuggestionColumns._ID, SuggestionColumns.DATE
98    };
99
100    /*
101     * Set a cap on the count of items in the suggestions table, to
102     * prevent db and layout operations from dragging to a crawl. Revisit this
103     * cap when/if db/layout performance improvements are made.
104     */
105    private static final int MAX_HISTORY_COUNT = 250;
106
107    // client-provided configuration values
108    private Context mContext;
109    private String mAuthority;
110    private boolean mTwoLineDisplay;
111    private Uri mSuggestionsUri;
112    private String[] mQueriesProjection;
113
114    /**
115     * Although provider utility classes are typically static, this one must be constructed
116     * because it needs to be initialized using the same values that you provided in your
117     * {@link android.content.SearchRecentSuggestionsProvider}.
118     *
119     * @param authority This must match the authority that you've declared in your manifest.
120     * @param mode You can use mode flags here to determine certain functional aspects of your
121     * database.  Note, this value should not change from run to run, because when it does change,
122     * your suggestions database may be wiped.
123     *
124     * @see android.content.SearchRecentSuggestionsProvider
125     * @see android.content.SearchRecentSuggestionsProvider#setupSuggestions
126     */
127    public SearchRecentSuggestions(Context context, String authority, int mode) {
128        if (TextUtils.isEmpty(authority) ||
129                ((mode & SearchRecentSuggestionsProvider.DATABASE_MODE_QUERIES) == 0)) {
130            throw new IllegalArgumentException();
131        }
132        // unpack mode flags
133        mTwoLineDisplay = (0 != (mode & SearchRecentSuggestionsProvider.DATABASE_MODE_2LINES));
134
135        // saved values
136        mContext = context;
137        mAuthority = new String(authority);
138
139        // derived values
140        mSuggestionsUri = Uri.parse("content://" + mAuthority + "/suggestions");
141
142        if (mTwoLineDisplay) {
143            mQueriesProjection = QUERIES_PROJECTION_2LINE;
144        } else {
145            mQueriesProjection = QUERIES_PROJECTION_1LINE;
146        }
147    }
148
149    /**
150     * Add a query to the recent queries list.
151     *
152     * @param queryString The string as typed by the user.  This string will be displayed as
153     * the suggestion, and if the user clicks on the suggestion, this string will be sent to your
154     * searchable activity (as a new search query).
155     * @param line2 If you have configured your recent suggestions provider with
156     * {@link android.content.SearchRecentSuggestionsProvider#DATABASE_MODE_2LINES}, you can
157     * pass a second line of text here.  It will be shown in a smaller font, below the primary
158     * suggestion.  When typing, matches in either line of text will be displayed in the list.
159     * If you did not configure two-line mode, or if a given suggestion does not have any
160     * additional text to display, you can pass null here.
161     */
162    public void saveRecentQuery(String queryString, String line2) {
163        if (TextUtils.isEmpty(queryString)) {
164            return;
165        }
166        if (!mTwoLineDisplay && !TextUtils.isEmpty(line2)) {
167            throw new IllegalArgumentException();
168        }
169
170        ContentResolver cr = mContext.getContentResolver();
171        long now = System.currentTimeMillis();
172
173        // Use content resolver (not cursor) to insert/update this query
174        try {
175            ContentValues values = new ContentValues();
176            values.put(SuggestionColumns.DISPLAY1, queryString);
177            if (mTwoLineDisplay) {
178                values.put(SuggestionColumns.DISPLAY2, line2);
179            }
180            values.put(SuggestionColumns.QUERY, queryString);
181            values.put(SuggestionColumns.DATE, now);
182            cr.insert(mSuggestionsUri, values);
183        } catch (RuntimeException e) {
184            Log.e(LOG_TAG, "saveRecentQuery", e);
185        }
186
187        // Shorten the list (if it has become too long)
188        truncateHistory(cr, MAX_HISTORY_COUNT);
189    }
190
191    /**
192     * Completely delete the history.  Use this call to implement a "clear history" UI.
193     *
194     * Any application that implements search suggestions based on previous actions (such as
195     * recent queries, page/items viewed, etc.) should provide a way for the user to clear the
196     * history.  This gives the user a measure of privacy, if they do not wish for their recent
197     * searches to be replayed by other users of the device (via suggestions).
198     */
199    public void clearHistory() {
200        ContentResolver cr = mContext.getContentResolver();
201        truncateHistory(cr, 0);
202    }
203
204    /**
205     * Reduces the length of the history table, to prevent it from growing too large.
206     *
207     * @param cr Convenience copy of the content resolver.
208     * @param maxEntries Max entries to leave in the table. 0 means remove all entries.
209     */
210    protected void truncateHistory(ContentResolver cr, int maxEntries) {
211        if (maxEntries < 0) {
212            throw new IllegalArgumentException();
213        }
214
215        try {
216            // null means "delete all".  otherwise "delete but leave n newest"
217            String selection = null;
218            if (maxEntries > 0) {
219                selection = "_id IN " +
220                        "(SELECT _id FROM suggestions" +
221                        " ORDER BY " + SuggestionColumns.DATE + " DESC" +
222                        " LIMIT -1 OFFSET " + String.valueOf(maxEntries) + ")";
223            }
224            cr.delete(mSuggestionsUri, selection, null);
225        } catch (RuntimeException e) {
226            Log.e(LOG_TAG, "truncateHistory", e);
227        }
228    }
229}
230