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