blob: 13210542e3289010fe674e985175b361aa02f9b3 [file] [log] [blame]
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quicksearchbox;
import com.android.common.Search;
import com.android.common.speech.Recognition;
import com.android.quicksearchbox.ui.CorpusViewFactory;
import android.app.Activity;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.SearchManager;
import android.appwidget.AppWidgetManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.os.SystemClock;
import android.speech.RecognizerIntent;
import android.text.Annotation;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import android.util.Log;
import android.view.View;
import android.widget.RemoteViews;
import java.util.ArrayList;
import java.util.Random;
/**
* Search widget provider.
*
*/
public class SearchWidgetProvider extends BroadcastReceiver {
private static final boolean DBG = false;
private static final String TAG = "QSB.SearchWidgetProvider";
/**
* Broadcast intent action for showing the next voice search hint
* (if voice search hints are enabled).
*/
private static final String ACTION_NEXT_VOICE_SEARCH_HINT =
"com.android.quicksearchbox.action.NEXT_VOICE_SEARCH_HINT";
/**
* Broadcast intent action for hiding voice search hints.
*/
private static final String ACTION_HIDE_VOICE_SEARCH_HINT =
"com.android.quicksearchbox.action.HIDE_VOICE_SEARCH_HINT";
/**
* Broadcast intent action for updating voice search hint display. Voice search hints will
* only be displayed with some probability.
*/
private static final String ACTION_CONSIDER_VOICE_SEARCH_HINT =
"com.android.quicksearchbox.action.CONSIDER_VOICE_SEARCH_HINT";
/**
* Preference key used for storing the index of the next voice search hint to show.
*/
private static final String NEXT_VOICE_SEARCH_HINT_INDEX_PREF = "next_voice_search_hint";
/**
* Preference key used to store the time at which the first voice search hint was displayed.
*/
private static final String FIRST_VOICE_HINT_DISPLAY_TIME = "first_voice_search_hint_time";
/**
* Preference key for the version of voice search we last got hints from.
*/
private static final String LAST_SEEN_VOICE_SEARCH_VERSION = "voice_search_version";
/**
* The {@link Search#SOURCE} value used when starting searches from the search widget.
*/
private static final String WIDGET_SEARCH_SOURCE = "launcher-widget";
private static Random sRandom;
@Override
public void onReceive(Context context, Intent intent) {
if (DBG) Log.d(TAG, "onReceive(" + intent.toUri(0) + ")");
String action = intent.getAction();
if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
scheduleVoiceHintUpdates(context);
} else if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
updateSearchWidgets(context);
} else if (ACTION_CONSIDER_VOICE_SEARCH_HINT.equals(action)) {
considerShowingVoiceSearchHints(context);
} else if (ACTION_NEXT_VOICE_SEARCH_HINT.equals(action)) {
getHintsFromVoiceSearch(context);
} else if (ACTION_HIDE_VOICE_SEARCH_HINT.equals(action)) {
hideVoiceSearchHint(context);
}
}
private static Random getRandom() {
if (sRandom == null) {
sRandom = new Random();
}
return sRandom;
}
private static boolean haveVoiceSearchHintsExpired(Context context) {
SharedPreferences prefs = SearchSettings.getSearchPreferences(context);
QsbApplication app = QsbApplication.get(context);
int currentVoiceSearchVersion = app.getVoiceSearch().getVersion();
if (currentVoiceSearchVersion != 0) {
long currentTime = System.currentTimeMillis();
int lastVoiceSearchVersion = prefs.getInt(LAST_SEEN_VOICE_SEARCH_VERSION, 0);
long firstHintTime = prefs.getLong(FIRST_VOICE_HINT_DISPLAY_TIME, 0);
if (firstHintTime == 0 || currentVoiceSearchVersion != lastVoiceSearchVersion) {
Editor e = prefs.edit();
e.putInt(LAST_SEEN_VOICE_SEARCH_VERSION, currentVoiceSearchVersion);
e.putLong(FIRST_VOICE_HINT_DISPLAY_TIME, currentTime);
e.commit();
firstHintTime = currentTime;
}
if (currentTime - firstHintTime > getConfig(context).getVoiceSearchHintActivePeriod()) {
if (DBG) Log.d(TAG, "Voice seach hint period expired; not showing hints.");
return true;
} else {
return false;
}
} else {
if (DBG) Log.d(TAG, "Could not determine voice search version; not showing hints.");
return true;
}
}
private static boolean shouldShowVoiceSearchHints(Context context) {
return (getConfig(context).allowVoiceSearchHints()
&& !haveVoiceSearchHintsExpired(context));
}
private static SearchWidgetState[] getSearchWidgetStates
(Context context, boolean enableVoiceSearchHints) {
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
int[] appWidgetIds = appWidgetManager.getAppWidgetIds(myComponentName(context));
SearchWidgetState[] states = new SearchWidgetState[appWidgetIds.length];
for (int i = 0; i<appWidgetIds.length; ++i) {
states[i] = getSearchWidgetState(context, appWidgetIds[i], enableVoiceSearchHints);
}
return states;
}
private static void considerShowingVoiceSearchHints(Context context) {
if (DBG) Log.d(TAG, "considerShowingVoiceSearchHints");
if (!shouldShowVoiceSearchHints(context)) return;
SearchWidgetState[] states = getSearchWidgetStates(context, true);
boolean needHint = false;
boolean changed = false;
for (SearchWidgetState state : states) {
changed |= state.considerShowingHint(context);
needHint |= state.isShowingHint();
}
if (changed) {
getHintsFromVoiceSearch(context);
sceduleNextVoiceSearchHint(context, true);
}
}
private void hideVoiceSearchHint(Context context) {
if (DBG) Log.d(TAG, "hideVoiceSearchHint");
SearchWidgetState[] states = getSearchWidgetStates(context, true);
boolean needHint = false;
for (SearchWidgetState state : states) {
if (state.isShowingHint()) {
state.hideVoiceSearchHint(context);
state.updateWidget(context, AppWidgetManager.getInstance(context));
}
needHint |= state.isShowingHint();
}
sceduleNextVoiceSearchHint(context, false);
}
private static void voiceSearchHintReceived(Context context, CharSequence hint) {
if (DBG) Log.d(TAG, "voiceSearchHintReceived('" + hint + "')");
CharSequence formatted = formatVoiceSearchHint(context, hint);
SearchWidgetState[] states = getSearchWidgetStates(context, true);
boolean needHint = false;
for (SearchWidgetState state : states) {
if (state.isShowingHint()) {
state.setVoiceSearchHint(formatted);
state.updateWidget(context, AppWidgetManager.getInstance(context));
needHint = true;
}
}
if (!needHint) {
sceduleNextVoiceSearchHint(context, false);
}
}
private static void scheduleVoiceHintUpdates(Context context) {
if (DBG) Log.d(TAG, "scheduleVoiceHintUpdates");
if (!shouldShowVoiceSearchHints(context)) return;
scheduleVoiceSearchHintUpdates(context, true);
}
/**
* Updates all search widgets.
*/
public static void updateSearchWidgets(Context context) {
if (DBG) Log.d(TAG, "updateSearchWidgets");
boolean showVoiceSearchHints = shouldShowVoiceSearchHints(context);
SearchWidgetState[] states = getSearchWidgetStates(context, showVoiceSearchHints);
boolean needVoiceSearchHint = false;
for (SearchWidgetState state : states) {
if (state.isShowingHint()) {
needVoiceSearchHint = true;
// widget update will occur when voice search hint received
} else {
state.updateWidget(context, AppWidgetManager.getInstance(context));
}
}
if (DBG) Log.d(TAG, "Need voice search hints=" + needVoiceSearchHint);
if (needVoiceSearchHint) {
getHintsFromVoiceSearch(context);
}
if (!showVoiceSearchHints) {
scheduleVoiceSearchHintUpdates(context, false);
}
}
/**
* Gets the component name of this search widget provider.
*/
private static ComponentName myComponentName(Context context) {
String pkg = context.getPackageName();
String cls = pkg + ".SearchWidgetProvider";
return new ComponentName(pkg, cls);
}
private static SearchWidgetState getSearchWidgetState(Context context,
int appWidgetId, boolean enableVoiceSearchHints) {
String corpusName =
SearchWidgetConfigActivity.getWidgetCorpusName(context, appWidgetId);
Corpus corpus = corpusName == null ? null : getCorpora(context).getCorpus(corpusName);
if (DBG) Log.d(TAG, "Creating appwidget state " + appWidgetId + ", corpus=" + corpus);
SearchWidgetState state = new SearchWidgetState(appWidgetId);
Bundle widgetAppData = new Bundle();
widgetAppData.putString(Search.SOURCE, WIDGET_SEARCH_SOURCE);
// Corpus indicator
state.setCorpusIconUri(getCorpusIconUri(context, corpus));
Intent corpusIconIntent = new Intent(SearchActivity.INTENT_ACTION_QSB_AND_SELECT_CORPUS);
corpusIconIntent.setPackage(context.getPackageName());
corpusIconIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
corpusIconIntent.putExtra(SearchManager.APP_DATA, widgetAppData);
corpusIconIntent.setData(SearchActivity.getCorpusUri(corpus));
state.setCorpusIndicatorIntent(corpusIconIntent);
// Query text view hint
if (corpus == null || corpus.isWebCorpus()) {
state.setQueryTextViewBackgroundResource(R.drawable.textfield_search_empty_google);
} else {
state.setQueryTextViewHint(corpus.getHint());
state.setQueryTextViewBackgroundResource(R.drawable.textfield_search_empty);
}
// Text field click
Intent qsbIntent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH);
qsbIntent.setPackage(context.getPackageName());
qsbIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
qsbIntent.putExtra(SearchManager.APP_DATA, widgetAppData);
qsbIntent.setData(SearchActivity.getCorpusUri(corpus));
state.setQueryTextViewIntent(qsbIntent);
// Voice search button
if (enableVoiceSearchHints) {
Intent voiceSearchIntent = getVoiceSearchIntent(context, corpus, widgetAppData);
state.setVoiceSearchIntent(voiceSearchIntent);
if (voiceSearchIntent != null
&& RecognizerIntent.ACTION_WEB_SEARCH.equals(voiceSearchIntent.getAction())) {
state.setVoiceSearchHintsEnabled(true);
boolean showingHint =
SearchWidgetConfigActivity.getWidgetShowingHint(context, appWidgetId);
if (DBG) Log.d(TAG, "Widget " + appWidgetId + " showing hint: " + showingHint);
state.setShowingHint(showingHint);
}
}
return state;
}
private static Intent getVoiceSearchIntent(Context context, Corpus corpus,
Bundle widgetAppData) {
VoiceSearch voiceSearch = QsbApplication.get(context).getVoiceSearch();
if (corpus == null || !voiceSearch.isVoiceSearchAvailable()) {
return voiceSearch.createVoiceWebSearchIntent(widgetAppData);
} else {
return corpus.createVoiceSearchIntent(widgetAppData);
}
}
private static Uri getCorpusIconUri(Context context, Corpus corpus) {
if (corpus == null) {
return getCorpusViewFactory(context).getGlobalSearchIconUri();
}
return corpus.getCorpusIconUri();
}
private static CharSequence formatVoiceSearchHint(Context context, CharSequence hint) {
if (TextUtils.isEmpty(hint)) return null;
SpannableStringBuilder spannedHint = new SpannableStringBuilder(
context.getString(R.string.voice_search_hint_quotation_start));
spannedHint.append(hint);
Object[] items = spannedHint.getSpans(0, spannedHint.length(), Object.class);
for (Object item : items) {
if (item instanceof Annotation) {
Annotation annotation = (Annotation) item;
if (annotation.getKey().equals("action")
&& annotation.getValue().equals("true")) {
final int start = spannedHint.getSpanStart(annotation);
final int end = spannedHint.getSpanEnd(annotation);
spannedHint.removeSpan(item);
spannedHint.setSpan(new StyleSpan(Typeface.BOLD), start, end, 0);
}
}
}
spannedHint.append(context.getString(R.string.voice_search_hint_quotation_end));
return spannedHint;
}
private static void rescheduleAction(Context context, boolean reshedule, String action, long period) {
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(action);
intent.setComponent(myComponentName(context));
PendingIntent pending = PendingIntent.getBroadcast(context, 0, intent, 0);
alarmManager.cancel(pending);
if (reshedule) {
if (DBG) Log.d(TAG, "Scheduling action " + action + " after period " + period);
alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + period, period, pending);
} else {
if (DBG) Log.d(TAG, "Cancelled action " + action);
}
}
public static void scheduleVoiceSearchHintUpdates(Context context, boolean enabled) {
rescheduleAction(context, enabled, ACTION_CONSIDER_VOICE_SEARCH_HINT,
getConfig(context).getVoiceSearchHintUpdatePeriod());
}
private static void sceduleNextVoiceSearchHint(Context context, boolean needUpdates) {
rescheduleAction(context, needUpdates, ACTION_NEXT_VOICE_SEARCH_HINT,
getConfig(context).getVoiceSearchHintChangePeriod());
}
/**
* Requests an asynchronous update of the voice search hints.
*/
private static void getHintsFromVoiceSearch(Context context) {
Intent intent = new Intent(RecognizerIntent.ACTION_GET_LANGUAGE_DETAILS);
if (DBG) Log.d(TAG, "Broadcasting " + intent);
context.sendOrderedBroadcast(intent, null,
new HintReceiver(), null, Activity.RESULT_OK, null, null);
}
private static class HintReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (getResultCode() != Activity.RESULT_OK) {
return;
}
ArrayList<CharSequence> hints = getResultExtras(true)
.getCharSequenceArrayList(Recognition.EXTRA_HINT_STRINGS);
CharSequence hint = getNextHint(context, hints);
voiceSearchHintReceived(context, hint);
}
}
/**
* Gets the next formatted hint, if there are any hints.
* Must be called on the application main thread.
*
* @return A hint, or {@code null} if no hints are available.
*/
private static CharSequence getNextHint(Context context, ArrayList<CharSequence> hints) {
if (hints == null || hints.isEmpty()) return null;
int i = getNextVoiceSearchHintIndex(context, hints.size());
return hints.get(i);
}
private static int getNextVoiceSearchHintIndex(Context context, int size) {
int i = getAndIncrementIntPreference(
SearchSettings.getSearchPreferences(context),
NEXT_VOICE_SEARCH_HINT_INDEX_PREF);
return i % size;
}
// TODO: Could this be made atomic to avoid races?
private static int getAndIncrementIntPreference(SharedPreferences prefs, String name) {
int i = prefs.getInt(name, 0);
prefs.edit().putInt(name, i + 1).commit();
return i;
}
private static Config getConfig(Context context) {
return QsbApplication.get(context).getConfig();
}
private static Corpora getCorpora(Context context) {
return QsbApplication.get(context).getCorpora();
}
private static CorpusViewFactory getCorpusViewFactory(Context context) {
return QsbApplication.get(context).getCorpusViewFactory();
}
private static class SearchWidgetState {
private final int mAppWidgetId;
private Uri mCorpusIconUri;
private Intent mCorpusIndicatorIntent;
private CharSequence mQueryTextViewHint;
private int mQueryTextViewBackgroundResource;
private Intent mQueryTextViewIntent;
private Intent mVoiceSearchIntent;
private boolean mVoiceSearchHintsEnabled;
private CharSequence mVoiceSearchHint;
private boolean mShowHint;
public SearchWidgetState(int appWidgetId) {
mAppWidgetId = appWidgetId;
}
public int getId() {
return mAppWidgetId;
}
public void setVoiceSearchHintsEnabled(boolean enabled) {
mVoiceSearchHintsEnabled = enabled;
}
public void setShowingHint(boolean show) {
mShowHint = show;
}
public boolean isShowingHint() {
return mShowHint;
}
public void setCorpusIconUri(Uri corpusIconUri) {
mCorpusIconUri = corpusIconUri;
}
public void setCorpusIndicatorIntent(Intent corpusIndicatorIntent) {
mCorpusIndicatorIntent = corpusIndicatorIntent;
}
public void setQueryTextViewHint(CharSequence queryTextViewHint) {
mQueryTextViewHint = queryTextViewHint;
}
public void setQueryTextViewBackgroundResource(int queryTextViewBackgroundResource) {
mQueryTextViewBackgroundResource = queryTextViewBackgroundResource;
}
public void setQueryTextViewIntent(Intent queryTextViewIntent) {
mQueryTextViewIntent = queryTextViewIntent;
}
public void setVoiceSearchIntent(Intent voiceSearchIntent) {
mVoiceSearchIntent = voiceSearchIntent;
}
public void setVoiceSearchHint(CharSequence voiceSearchHint) {
mVoiceSearchHint = voiceSearchHint;
}
private boolean chooseToShowHint(Context context) {
// this is called every getConfig().getVoiceSearchHintUpdatePeriod() milliseconds
// we want to return true every getConfig().getVoiceSearchHintShowPeriod() milliseconds
// so:
Config cfg = getConfig(context);
float p = (float) cfg.getVoiceSearchHintUpdatePeriod()
/ (float) cfg.getVoiceSearchHintShowPeriod();
float f = getRandom().nextFloat();
// if p > 1 we won't return true as often as we should (we can't return more times than
// we're called!) but we will always return true.
boolean r = (f < p);
if (DBG) Log.d(TAG, "chooseToShowHint p=" + p +"; f=" + f + "; r=" + r);
return r;
}
private Intent createIntent(Context context, String action) {
Intent intent = new Intent(action);
intent.setComponent(myComponentName(context));
return intent;
}
private void sheduleHintHiding(Context context) {
Intent hideIntent = createIntent(context, ACTION_HIDE_VOICE_SEARCH_HINT);
AlarmManager alarmManager =
(AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent hideHint = PendingIntent.getBroadcast(context, 0, hideIntent, 0);
long period = getConfig(context).getVoiceSearchHintVisibleTime();
if (DBG) {
Log.d(TAG, "Scheduling action " + ACTION_HIDE_VOICE_SEARCH_HINT +
" after period " + period);
}
alarmManager.set(AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + period, hideHint);
}
private void updateShowingHint(Context context) {
SearchWidgetConfigActivity.setWidgetShowingHint(context, mAppWidgetId, mShowHint);
}
public boolean considerShowingHint(Context context) {
if (!mVoiceSearchHintsEnabled || mShowHint) return false;
if (!chooseToShowHint(context)) return false;
sheduleHintHiding(context);
mShowHint = true;
updateShowingHint(context);
return true;
}
public void hideVoiceSearchHint(Context context) {
mShowHint = false;
updateShowingHint(context);
}
public void updateWidget(Context context,AppWidgetManager appWidgetMgr) {
if (DBG) Log.d(TAG, "Updating appwidget " + mAppWidgetId);
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.search_widget);
// Corpus indicator
// Before Froyo, android.resource URI could not be used in ImageViews.
if (QsbApplication.isFroyoOrLater()) {
views.setImageViewUri(R.id.corpus_indicator, mCorpusIconUri);
}
setOnClickActivityIntent(context, views, R.id.corpus_indicator,
mCorpusIndicatorIntent);
// Query TextView
views.setCharSequence(R.id.search_widget_text, "setHint", mQueryTextViewHint);
// setBackgroundResource did not have @RemotableViewMethod before Froyo
if (QsbApplication.isFroyoOrLater()) {
views.setInt(R.id.search_widget_text, "setBackgroundResource",
mQueryTextViewBackgroundResource);
}
setOnClickActivityIntent(context, views, R.id.search_widget_text,
mQueryTextViewIntent);
// Voice Search button
if (mVoiceSearchIntent != null) {
setOnClickActivityIntent(context, views, R.id.search_widget_voice_btn,
mVoiceSearchIntent);
views.setViewVisibility(R.id.search_widget_voice_btn, View.VISIBLE);
} else {
views.setViewVisibility(R.id.search_widget_voice_btn, View.GONE);
}
// Voice Search hints
if (mShowHint && !TextUtils.isEmpty(mVoiceSearchHint)) {
views.setTextViewText(R.id.voice_search_hint_text, mVoiceSearchHint);
Intent closeHintIntent = createIntent(context, ACTION_HIDE_VOICE_SEARCH_HINT);
setOnClickBroadcastIntent(context, views, R.id.voice_search_hint_close,
closeHintIntent);
setOnClickActivityIntent(context, views, R.id.voice_search_hint_text,
mVoiceSearchIntent);
views.setViewVisibility(R.id.voice_search_hint, View.VISIBLE);
} else {
views.setViewVisibility(R.id.voice_search_hint, View.GONE);
}
appWidgetMgr.updateAppWidget(mAppWidgetId, views);
}
private void setOnClickBroadcastIntent(Context context, RemoteViews views, int viewId,
Intent intent) {
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
views.setOnClickPendingIntent(viewId, pendingIntent);
}
private void setOnClickActivityIntent(Context context, RemoteViews views, int viewId,
Intent intent) {
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
views.setOnClickPendingIntent(viewId, pendingIntent);
}
}
}