SearchWidgetProvider.java revision 447f3b6524d5153b759ea9e7f6c47a0c3cc46b6d
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 com.android.common.Search;
20import com.android.common.speech.Recognition;
21import com.android.quicksearchbox.ui.CorpusViewFactory;
22
23import android.app.Activity;
24import android.app.AlarmManager;
25import android.app.PendingIntent;
26import android.app.SearchManager;
27import android.appwidget.AppWidgetManager;
28import android.content.BroadcastReceiver;
29import android.content.ComponentName;
30import android.content.Context;
31import android.content.Intent;
32import android.content.SharedPreferences;
33import android.content.SharedPreferences.Editor;
34import android.graphics.Typeface;
35import android.net.Uri;
36import android.os.Bundle;
37import android.os.SystemClock;
38import android.speech.RecognizerIntent;
39import android.text.Annotation;
40import android.text.SpannableStringBuilder;
41import android.text.TextUtils;
42import android.text.style.StyleSpan;
43import android.util.Log;
44import android.view.View;
45import android.widget.RemoteViews;
46
47import java.util.ArrayList;
48import java.util.Random;
49
50/**
51 * Search widget provider.
52 *
53 */
54public class SearchWidgetProvider extends BroadcastReceiver {
55
56    private static final boolean DBG = false;
57    private static final String TAG = "QSB.SearchWidgetProvider";
58
59    /**
60     * Broadcast intent action for showing the next voice search hint
61     * (if voice search hints are enabled).
62     */
63    private static final String ACTION_NEXT_VOICE_SEARCH_HINT =
64            "com.android.quicksearchbox.action.NEXT_VOICE_SEARCH_HINT";
65
66    /**
67     * Broadcast intent action for hiding voice search hints.
68     */
69    private static final String ACTION_HIDE_VOICE_SEARCH_HINT =
70        "com.android.quicksearchbox.action.HIDE_VOICE_SEARCH_HINT";
71
72    /**
73     * Broadcast intent action for updating voice search hint display. Voice search hints will
74     * only be displayed with some probability.
75     */
76    private static final String ACTION_CONSIDER_VOICE_SEARCH_HINT =
77            "com.android.quicksearchbox.action.CONSIDER_VOICE_SEARCH_HINT";
78
79    /**
80     * Broadcast intent action for displaying voice search hints immediately.
81     */
82    private static final String ACTION_SHOW_VOICE_SEARCH_HINT_NOW =
83            "com.android.quicksearchbox.action.SHOW_VOICE_SEARCH_HINT_NOW";
84
85    private static final String ACTION_RESET_VOICE_SEARCH_HINT_FIRST_SEEN =
86            "com.android.quicksearchbox.action.debugonly.RESET_HINT_FIRST_SEEN_TIME";
87
88    /**
89     * Preference key used for storing the index of the next voice search hint to show.
90     */
91    private static final String NEXT_VOICE_SEARCH_HINT_INDEX_PREF = "next_voice_search_hint";
92
93    /**
94     * Preference key used to store the time at which the first voice search hint was displayed.
95     */
96    private static final String FIRST_VOICE_HINT_DISPLAY_TIME = "first_voice_search_hint_time";
97
98    /**
99     * Preference key for the version of voice search we last got hints from.
100     */
101    private static final String LAST_SEEN_VOICE_SEARCH_VERSION = "voice_search_version";
102
103    /**
104     * The {@link Search#SOURCE} value used when starting searches from the search widget.
105     */
106    private static final String WIDGET_SEARCH_SOURCE = "launcher-widget";
107
108    private static Random sRandom;
109
110    @Override
111    public void onReceive(Context context, Intent intent) {
112        if (DBG) Log.d(TAG, "onReceive(" + intent.toUri(0) + ")");
113        String action = intent.getAction();
114        if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
115            scheduleVoiceHintUpdates(context);
116        } else if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
117            updateSearchWidgets(context);
118        } else if (ACTION_CONSIDER_VOICE_SEARCH_HINT.equals(action)) {
119            considerShowingVoiceSearchHints(context);
120        } else if (ACTION_NEXT_VOICE_SEARCH_HINT.equals(action)) {
121            getHintsFromVoiceSearch(context);
122        } else if (ACTION_HIDE_VOICE_SEARCH_HINT.equals(action)) {
123            hideVoiceSearchHint(context);
124        } else if (ACTION_SHOW_VOICE_SEARCH_HINT_NOW.equals(action)) {
125            showVoiceSearchHintNow(context);
126        } else if (ACTION_RESET_VOICE_SEARCH_HINT_FIRST_SEEN.equals(action)) {
127            resetVoiceSearchHintFirstSeenTime(context);
128        }
129    }
130
131    private static Random getRandom() {
132        if (sRandom == null) {
133            sRandom = new Random();
134        }
135        return sRandom;
136    }
137
138    private static void resetVoiceSearchHintFirstSeenTime(Context context) {
139        SharedPreferences prefs = SearchSettings.getSearchPreferences(context);
140        Editor e = prefs.edit();
141        e.putLong(FIRST_VOICE_HINT_DISPLAY_TIME, System.currentTimeMillis());
142        e.commit();
143    }
144
145    private static boolean haveVoiceSearchHintsExpired(Context context) {
146        SharedPreferences prefs = SearchSettings.getSearchPreferences(context);
147        QsbApplication app = QsbApplication.get(context);
148        int currentVoiceSearchVersion = app.getVoiceSearch().getVersion();
149
150        if (currentVoiceSearchVersion != 0) {
151            long currentTime = System.currentTimeMillis();
152            int lastVoiceSearchVersion = prefs.getInt(LAST_SEEN_VOICE_SEARCH_VERSION, 0);
153            long firstHintTime = prefs.getLong(FIRST_VOICE_HINT_DISPLAY_TIME, 0);
154            if (firstHintTime == 0 || currentVoiceSearchVersion != lastVoiceSearchVersion) {
155                Editor e = prefs.edit();
156                e.putInt(LAST_SEEN_VOICE_SEARCH_VERSION, currentVoiceSearchVersion);
157                e.putLong(FIRST_VOICE_HINT_DISPLAY_TIME, currentTime);
158                e.commit();
159                firstHintTime = currentTime;
160            }
161            if (currentTime - firstHintTime > getConfig(context).getVoiceSearchHintActivePeriod()) {
162                if (DBG) Log.d(TAG, "Voice seach hint period expired; not showing hints.");
163                return true;
164            } else {
165                return false;
166            }
167        } else {
168            if (DBG) Log.d(TAG, "Could not determine voice search version; not showing hints.");
169            return true;
170        }
171    }
172
173    private static boolean shouldShowVoiceSearchHints(Context context) {
174        return (getConfig(context).allowVoiceSearchHints()
175                && !haveVoiceSearchHintsExpired(context));
176    }
177
178    private static SearchWidgetState[] getSearchWidgetStates
179            (Context context, boolean enableVoiceSearchHints) {
180
181        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
182        int[] appWidgetIds = appWidgetManager.getAppWidgetIds(myComponentName(context));
183        SearchWidgetState[] states = new SearchWidgetState[appWidgetIds.length];
184        for (int i = 0; i<appWidgetIds.length; ++i) {
185            states[i] = getSearchWidgetState(context, appWidgetIds[i], enableVoiceSearchHints);
186        }
187        return states;
188    }
189
190    private static void considerShowingVoiceSearchHints(Context context) {
191        if (DBG) Log.d(TAG, "considerShowingVoiceSearchHints");
192        if (!shouldShowVoiceSearchHints(context)) return;
193        SearchWidgetState[] states = getSearchWidgetStates(context, true);
194        boolean changed = false;
195        for (SearchWidgetState state : states) {
196            changed |= state.considerShowingHint(context);
197        }
198        if (changed) {
199            getHintsFromVoiceSearch(context);
200            scheduleNextVoiceSearchHint(context, true);
201        }
202    }
203
204    private static void showVoiceSearchHintNow(Context context) {
205        if (DBG) Log.d(TAG, "showVoiceSearchHintNow");
206        SearchWidgetState[] states = getSearchWidgetStates(context, true);
207        for (SearchWidgetState state : states) {
208            if (state.mVoiceSearchIntent != null) {
209                state.showHintNow(context);
210            }
211        }
212        getHintsFromVoiceSearch(context);
213        scheduleNextVoiceSearchHint(context, true);
214    }
215
216    private void hideVoiceSearchHint(Context context) {
217        if (DBG) Log.d(TAG, "hideVoiceSearchHint");
218        SearchWidgetState[] states = getSearchWidgetStates(context, true);
219        boolean needHint = false;
220        for (SearchWidgetState state : states) {
221            if (state.isShowingHint()) {
222                state.hideVoiceSearchHint(context);
223                state.updateWidget(context, AppWidgetManager.getInstance(context));
224            }
225            needHint |= state.isShowingHint();
226        }
227        scheduleNextVoiceSearchHint(context, false);
228    }
229
230    private static void voiceSearchHintReceived(Context context, CharSequence hint) {
231        if (DBG) Log.d(TAG, "voiceSearchHintReceived('" + hint + "')");
232        CharSequence formatted = formatVoiceSearchHint(context, hint);
233        SearchWidgetState[] states = getSearchWidgetStates(context, true);
234        boolean needHint = false;
235        for (SearchWidgetState state : states) {
236            if (state.isShowingHint()) {
237                state.setVoiceSearchHint(formatted);
238                state.updateWidget(context, AppWidgetManager.getInstance(context));
239                needHint = true;
240            }
241        }
242        if (!needHint) {
243            scheduleNextVoiceSearchHint(context, false);
244        }
245    }
246
247    private static void scheduleVoiceHintUpdates(Context context) {
248        if (DBG) Log.d(TAG, "scheduleVoiceHintUpdates");
249        if (!shouldShowVoiceSearchHints(context)) return;
250        scheduleVoiceSearchHintUpdates(context, true);
251    }
252
253    /**
254     * Updates all search widgets.
255     */
256    public static void updateSearchWidgets(Context context) {
257        if (DBG) Log.d(TAG, "updateSearchWidgets");
258        boolean showVoiceSearchHints = shouldShowVoiceSearchHints(context);
259        SearchWidgetState[] states = getSearchWidgetStates(context, showVoiceSearchHints);
260
261        boolean needVoiceSearchHint = false;
262        for (SearchWidgetState state : states) {
263            if (state.isShowingHint()) {
264                needVoiceSearchHint = true;
265                // widget update will occur when voice search hint received
266            } else {
267                state.updateWidget(context, AppWidgetManager.getInstance(context));
268            }
269        }
270        if (DBG) Log.d(TAG, "Need voice search hints=" + needVoiceSearchHint);
271        if (needVoiceSearchHint) {
272            getHintsFromVoiceSearch(context);
273        }
274        if (!showVoiceSearchHints) {
275            scheduleVoiceSearchHintUpdates(context, false);
276        }
277    }
278
279    /**
280     * Gets the component name of this search widget provider.
281     */
282    private static ComponentName myComponentName(Context context) {
283        String pkg = context.getPackageName();
284        String cls = pkg + ".SearchWidgetProvider";
285        return new ComponentName(pkg, cls);
286    }
287
288    private static Intent createQsbActivityIntent(Context context, String action,
289            Bundle widgetAppData, Corpus corpus) {
290        Intent qsbIntent = new Intent(action);
291        qsbIntent.setPackage(context.getPackageName());
292        qsbIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
293                | Intent.FLAG_ACTIVITY_CLEAR_TOP
294                | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
295        qsbIntent.putExtra(SearchManager.APP_DATA, widgetAppData);
296        qsbIntent.setData(SearchActivity.getCorpusUri(corpus));
297        return qsbIntent;
298    }
299
300    private static SearchWidgetState getSearchWidgetState(Context context,
301            int appWidgetId, boolean enableVoiceSearchHints) {
302        String corpusName =
303                SearchWidgetConfigActivity.getWidgetCorpusName(context, appWidgetId);
304        Corpus corpus = corpusName == null ? null : getCorpora(context).getCorpus(corpusName);
305        if (DBG) Log.d(TAG, "Creating appwidget state " + appWidgetId + ", corpus=" + corpus);
306        SearchWidgetState state = new SearchWidgetState(appWidgetId);
307
308        Bundle widgetAppData = new Bundle();
309        widgetAppData.putString(Search.SOURCE, WIDGET_SEARCH_SOURCE);
310
311        // Query text view hint
312        if (corpus == null || corpus.isWebCorpus()) {
313            state.setQueryTextViewBackgroundResource(R.drawable.textfield_search_empty_google);
314        } else {
315            state.setQueryTextViewHint(corpus.getHint());
316            state.setQueryTextViewBackgroundResource(R.drawable.textfield_search_empty);
317        }
318
319        // Text field click
320        Intent qsbIntent = createQsbActivityIntent(
321                context,
322                SearchManager.INTENT_ACTION_GLOBAL_SEARCH,
323                widgetAppData,
324                corpus);
325        state.setQueryTextViewIntent(qsbIntent);
326
327        // Voice search button
328        Intent voiceSearchIntent = getVoiceSearchIntent(context, corpus, widgetAppData);
329        state.setVoiceSearchIntent(voiceSearchIntent);
330        if (enableVoiceSearchHints && voiceSearchIntent != null
331                && RecognizerIntent.ACTION_WEB_SEARCH.equals(voiceSearchIntent.getAction())) {
332            state.setVoiceSearchHintsEnabled(true);
333
334            boolean showingHint =
335                    SearchWidgetConfigActivity.getWidgetShowingHint(context, appWidgetId);
336            if (DBG) Log.d(TAG, "Widget " + appWidgetId + " showing hint: " + showingHint);
337            state.setShowingHint(showingHint);
338
339        }
340
341        // Corpus indicator
342        state.setCorpusIconUri(getCorpusIconUri(context, corpus));
343
344        Intent corpusIconIntent = createQsbActivityIntent(context,
345                SearchActivity.INTENT_ACTION_QSB_AND_SELECT_CORPUS, widgetAppData, corpus);
346        state.setCorpusIndicatorIntent(corpusIconIntent);
347
348        return state;
349    }
350
351    private static Intent getVoiceSearchIntent(Context context, Corpus corpus,
352            Bundle widgetAppData) {
353        VoiceSearch voiceSearch = QsbApplication.get(context).getVoiceSearch();
354
355        if (corpus == null || !voiceSearch.isVoiceSearchAvailable()) {
356            return voiceSearch.createVoiceWebSearchIntent(widgetAppData);
357        } else {
358            return corpus.createVoiceSearchIntent(widgetAppData);
359        }
360    }
361
362    private static Intent getVoiceSearchHelpIntent(Context context) {
363        VoiceSearch voiceSearch = QsbApplication.get(context).getVoiceSearch();
364        return voiceSearch.createVoiceSearchHelpIntent();
365    }
366
367    private static PendingIntent createBroadcast(Context context, String action) {
368        Intent intent = new Intent(action);
369        intent.setComponent(myComponentName(context));
370        return PendingIntent.getBroadcast(context, 0, intent, 0);
371    }
372
373    private static Uri getCorpusIconUri(Context context, Corpus corpus) {
374        if (corpus == null) {
375            return getCorpusViewFactory(context).getGlobalSearchIconUri();
376        }
377        return corpus.getCorpusIconUri();
378    }
379
380    private static CharSequence formatVoiceSearchHint(Context context, CharSequence hint) {
381        if (TextUtils.isEmpty(hint)) return null;
382        SpannableStringBuilder spannedHint = new SpannableStringBuilder(
383                context.getString(R.string.voice_search_hint_quotation_start));
384        spannedHint.append(hint);
385        Object[] items = spannedHint.getSpans(0, spannedHint.length(), Object.class);
386        for (Object item : items) {
387            if (item instanceof Annotation) {
388                Annotation annotation = (Annotation) item;
389                if (annotation.getKey().equals("action")
390                        && annotation.getValue().equals("true")) {
391                    final int start = spannedHint.getSpanStart(annotation);
392                    final int end = spannedHint.getSpanEnd(annotation);
393                    spannedHint.removeSpan(item);
394                    spannedHint.setSpan(new StyleSpan(Typeface.BOLD), start, end, 0);
395                }
396            }
397        }
398        spannedHint.append(context.getString(R.string.voice_search_hint_quotation_end));
399        return spannedHint;
400    }
401
402    private static void rescheduleAction(Context context, boolean reschedule, String action, long period) {
403        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
404        PendingIntent intent = createBroadcast(context, action);
405        alarmManager.cancel(intent);
406        if (reschedule) {
407            if (DBG) Log.d(TAG, "Scheduling action " + action + " after period " + period);
408            alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
409                    SystemClock.elapsedRealtime() + period, period, intent);
410        } else {
411            if (DBG) Log.d(TAG, "Cancelled action " + action);
412        }
413    }
414
415    public static void scheduleVoiceSearchHintUpdates(Context context, boolean enabled) {
416        rescheduleAction(context, enabled, ACTION_CONSIDER_VOICE_SEARCH_HINT,
417                getConfig(context).getVoiceSearchHintUpdatePeriod());
418    }
419
420    private static void scheduleNextVoiceSearchHint(Context context, boolean needUpdates) {
421        rescheduleAction(context, needUpdates, ACTION_NEXT_VOICE_SEARCH_HINT,
422                getConfig(context).getVoiceSearchHintChangePeriod());
423    }
424
425    /**
426     * Requests an asynchronous update of the voice search hints.
427     */
428    private static void getHintsFromVoiceSearch(Context context) {
429        Intent intent = new Intent(RecognizerIntent.ACTION_GET_LANGUAGE_DETAILS);
430        if (DBG) Log.d(TAG, "Broadcasting " + intent);
431        context.sendOrderedBroadcast(intent, null,
432                new HintReceiver(), null, Activity.RESULT_OK, null, null);
433    }
434
435    private static class HintReceiver extends BroadcastReceiver {
436        @Override
437        public void onReceive(Context context, Intent intent) {
438            if (getResultCode() != Activity.RESULT_OK) {
439                return;
440            }
441            ArrayList<CharSequence> hints = getResultExtras(true)
442                    .getCharSequenceArrayList(Recognition.EXTRA_HINT_STRINGS);
443            CharSequence hint = getNextHint(context, hints);
444            voiceSearchHintReceived(context, hint);
445        }
446    }
447
448    /**
449     * Gets the next formatted hint, if there are any hints.
450     * Must be called on the application main thread.
451     *
452     * @return A hint, or {@code null} if no hints are available.
453     */
454    private static CharSequence getNextHint(Context context, ArrayList<CharSequence> hints) {
455        if (hints == null || hints.isEmpty()) return null;
456        int i = getNextVoiceSearchHintIndex(context, hints.size());
457        return hints.get(i);
458    }
459
460    private static int getNextVoiceSearchHintIndex(Context context, int size) {
461        int i = getAndIncrementIntPreference(
462                SearchSettings.getSearchPreferences(context),
463                NEXT_VOICE_SEARCH_HINT_INDEX_PREF);
464        return i % size;
465    }
466
467    // TODO: Could this be made atomic to avoid races?
468    private static int getAndIncrementIntPreference(SharedPreferences prefs, String name) {
469        int i = prefs.getInt(name, 0);
470        prefs.edit().putInt(name, i + 1).commit();
471        return i;
472    }
473
474    private static Config getConfig(Context context) {
475        return QsbApplication.get(context).getConfig();
476    }
477
478    private static Corpora getCorpora(Context context) {
479        return QsbApplication.get(context).getCorpora();
480    }
481
482    private static CorpusViewFactory getCorpusViewFactory(Context context) {
483        return QsbApplication.get(context).getCorpusViewFactory();
484    }
485
486    private static class SearchWidgetState {
487        private final int mAppWidgetId;
488        private Uri mCorpusIconUri;
489        private Intent mCorpusIndicatorIntent;
490        private CharSequence mQueryTextViewHint;
491        private int mQueryTextViewBackgroundResource;
492        private Intent mQueryTextViewIntent;
493        private Intent mVoiceSearchIntent;
494        private boolean mVoiceSearchHintsEnabled;
495        private CharSequence mVoiceSearchHint;
496        private boolean mShowHint;
497
498        public SearchWidgetState(int appWidgetId) {
499            mAppWidgetId = appWidgetId;
500        }
501
502        public void setVoiceSearchHintsEnabled(boolean enabled) {
503            mVoiceSearchHintsEnabled = enabled;
504        }
505
506        public void setShowingHint(boolean show) {
507            mShowHint = show;
508        }
509
510        public boolean isShowingHint() {
511            return mShowHint;
512        }
513
514        public void setCorpusIconUri(Uri corpusIconUri) {
515            mCorpusIconUri = corpusIconUri;
516        }
517
518        public void setCorpusIndicatorIntent(Intent corpusIndicatorIntent) {
519            mCorpusIndicatorIntent = corpusIndicatorIntent;
520        }
521
522        public void setQueryTextViewHint(CharSequence queryTextViewHint) {
523            mQueryTextViewHint = queryTextViewHint;
524        }
525
526        public void setQueryTextViewBackgroundResource(int queryTextViewBackgroundResource) {
527            mQueryTextViewBackgroundResource = queryTextViewBackgroundResource;
528        }
529
530        public void setQueryTextViewIntent(Intent queryTextViewIntent) {
531            mQueryTextViewIntent = queryTextViewIntent;
532        }
533
534        public void setVoiceSearchIntent(Intent voiceSearchIntent) {
535            mVoiceSearchIntent = voiceSearchIntent;
536        }
537
538        public void setVoiceSearchHint(CharSequence voiceSearchHint) {
539            mVoiceSearchHint = voiceSearchHint;
540        }
541
542        private boolean chooseToShowHint(Context context) {
543            // this is called every getConfig().getVoiceSearchHintUpdatePeriod() milliseconds
544            // we want to return true every getConfig().getVoiceSearchHintShowPeriod() milliseconds
545            // so:
546            Config cfg = getConfig(context);
547            float p = (float) cfg.getVoiceSearchHintUpdatePeriod()
548                    / (float) cfg.getVoiceSearchHintShowPeriod();
549            float f = getRandom().nextFloat();
550            // if p > 1 we won't return true as often as we should (we can't return more times than
551            // we're called!) but we will always return true.
552            boolean r = (f < p);
553            if (DBG) Log.d(TAG, "chooseToShowHint p=" + p +"; f=" + f + "; r=" + r);
554            return r;
555        }
556
557        private void scheduleHintHiding(Context context) {
558            AlarmManager alarmManager =
559                    (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
560            PendingIntent hideHint = createBroadcast(context, ACTION_HIDE_VOICE_SEARCH_HINT);
561
562            long period = getConfig(context).getVoiceSearchHintVisibleTime();
563            if (DBG) {
564                Log.d(TAG, "Scheduling action " + ACTION_HIDE_VOICE_SEARCH_HINT +
565                        " after period " + period);
566            }
567            alarmManager.set(AlarmManager.ELAPSED_REALTIME,
568                    SystemClock.elapsedRealtime() + period, hideHint);
569
570        }
571
572        public void updateShowingHint(Context context) {
573            SearchWidgetConfigActivity.setWidgetShowingHint(context, mAppWidgetId, mShowHint);
574        }
575
576        public boolean considerShowingHint(Context context) {
577            if (!mVoiceSearchHintsEnabled || mShowHint) return false;
578            if (!chooseToShowHint(context)) return false;
579            showHintNow(context);
580            return true;
581        }
582
583        public void showHintNow(Context context) {
584            scheduleHintHiding(context);
585            mShowHint = true;
586            updateShowingHint(context);
587        }
588
589        public void hideVoiceSearchHint(Context context) {
590            mShowHint = false;
591            updateShowingHint(context);
592        }
593
594        public void updateWidget(Context context,AppWidgetManager appWidgetMgr) {
595            if (DBG) Log.d(TAG, "Updating appwidget " + mAppWidgetId);
596            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.search_widget);
597            // Corpus indicator
598            // Before Froyo, android.resource URI could not be used in ImageViews.
599            if (QsbApplication.isFroyoOrLater()) {
600                views.setImageViewUri(R.id.corpus_indicator, mCorpusIconUri);
601            }
602            setOnClickActivityIntent(context, views, R.id.corpus_indicator,
603                    mCorpusIndicatorIntent);
604            // Query TextView
605            views.setCharSequence(R.id.search_widget_text, "setHint", mQueryTextViewHint);
606            setBackgroundResource(views, R.id.search_widget_text, mQueryTextViewBackgroundResource);
607
608            setOnClickActivityIntent(context, views, R.id.search_widget_text,
609                    mQueryTextViewIntent);
610            // Voice Search button
611            if (mVoiceSearchIntent != null) {
612                setOnClickActivityIntent(context, views, R.id.search_widget_voice_btn,
613                        mVoiceSearchIntent);
614                views.setViewVisibility(R.id.search_widget_voice_btn, View.VISIBLE);
615            } else {
616                views.setViewVisibility(R.id.search_widget_voice_btn, View.GONE);
617            }
618
619            // Voice Search hints
620            if (mShowHint && !TextUtils.isEmpty(mVoiceSearchHint)) {
621                views.setTextViewText(R.id.voice_search_hint_text, mVoiceSearchHint);
622
623                Intent voiceSearchHelp = getVoiceSearchHelpIntent(context);
624                if (voiceSearchHelp == null) voiceSearchHelp = mVoiceSearchIntent;
625                setOnClickActivityIntent(context, views, R.id.voice_search_hint,
626                        voiceSearchHelp);
627
628                PendingIntent hideIntent = createBroadcast(context, ACTION_HIDE_VOICE_SEARCH_HINT);
629                views.setOnClickPendingIntent(R.id.voice_search_hint_close_button, hideIntent);
630
631                views.setViewVisibility(R.id.voice_search_hint_container, View.VISIBLE);
632                views.setViewVisibility(R.id.search_widget_text, View.GONE);
633                views.setViewVisibility(R.id.corpus_indicator, View.GONE);
634            } else {
635                views.setViewVisibility(R.id.voice_search_hint_container, View.GONE);
636                views.setViewVisibility(R.id.search_widget_text, View.VISIBLE);
637                views.setViewVisibility(R.id.corpus_indicator, View.VISIBLE);
638            }
639            appWidgetMgr.updateAppWidget(mAppWidgetId, views);
640        }
641
642        private void setBackgroundResource(RemoteViews views, int viewId, int bgResource) {
643            // setBackgroundResource did not have @RemotableViewMethod before Froyo
644            if (QsbApplication.isFroyoOrLater()) {
645                views.setInt(viewId, "setBackgroundResource", bgResource);
646            }
647        }
648
649        private void setOnClickActivityIntent(Context context, RemoteViews views, int viewId,
650                Intent intent) {
651            PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
652            views.setOnClickPendingIntent(viewId, pendingIntent);
653        }
654    }
655
656}
657