blob: b06d7d6e67d1236da40b9c283e932e8f45b25435 [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.globalsearch;
import android.database.Cursor;
import android.content.Context;
import android.content.ComponentName;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
import java.util.concurrent.Executor;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.HashSet;
import java.util.Iterator;
/**
* Holds onto the current {@link SuggestionSession} and manages its lifecycle. When a session ends,
* it gets the session stats and reports them to the {@link ShortcutRepository}.
*/
public class SessionManager implements SuggestionSession.SessionCallback {
private static final String TAG = "SessionManager";
private static final boolean DBG = false;
private static SessionManager sInstance;
private final Context mContext;
public static synchronized SessionManager getInstance() {
return sInstance;
}
/**
* Refreshes the global session manager.
*
* @param sources The suggestion sources.
* @param shortcutRepo The shortcut repository.
* @param queryExecutor The executor used to execute search suggestion tasks.
* @param refreshExecutor The executor used execute shortcut refresh tasks.
* @param handler The handler passed along to the session.
* @return The up to date session manager.
*/
public static synchronized SessionManager refreshSessionmanager(Context context,
SuggestionSources sources, ShortcutRepository shortcutRepo,
PerTagExecutor queryExecutor,
Executor refreshExecutor, Handler handler) {
if (DBG) Log.d(TAG, "refreshSessionmanager()");
sInstance = new SessionManager(context, sources, shortcutRepo,
queryExecutor, refreshExecutor, handler);
return sInstance;
}
private SessionManager(Context context,
SuggestionSources sources, ShortcutRepository shortcutRepo,
PerTagExecutor queryExecutor, Executor refreshExecutor, Handler handler) {
mContext = context;
mSources = sources;
mShortcutRepo = shortcutRepo;
mQueryExecutor = queryExecutor;
mRefreshExecutor = refreshExecutor;
mHandler = handler;
}
private final SuggestionSources mSources;
private final ShortcutRepository mShortcutRepo;
private final PerTagExecutor mQueryExecutor;
private final Executor mRefreshExecutor;
private final Handler mHandler;
private SuggestionSession mSession;
/**
* Queries the current session for results.
*
* @see SuggestionSession#query(String)
*/
public synchronized Cursor query(Context context, String query) {
// create a new session if there is none,
// or when starting a new typing session
if (mSession == null || TextUtils.isEmpty(query)) {
mSession = createSession();
}
return mSession.query(query);
}
/** {@inheritDoc} */
public synchronized void closeSession() {
if (DBG) Log.d(TAG, "closeSession()");
mSession = null;
}
private SuggestionSession createSession() {
if (DBG) Log.d(TAG, "createSession()");
final SuggestionSource webSearchSource = mSources.getSelectedWebSearchSource();
// Fire off a warm-up query to the web search source, which that source can use for
// whatever it sees fit. For example, EnhancedGoogleSearchProvider uses this to
// determine whether a opt-in needs to be shown for use of location.
if (webSearchSource != null) {
warmUpWebSource(webSearchSource);
}
Sources sources = orderSources(
mSources.getEnabledSuggestionSources(),
webSearchSource,
mShortcutRepo.getSourceRanking(),
SuggestionSession.NUM_PROMOTED_SOURCES);
// implement the delayed executor using the handler
final DelayedExecutor delayedExecutor = new DelayedExecutor() {
public void postDelayed(Runnable runnable, long delayMillis) {
mHandler.postDelayed(runnable, delayMillis);
}
public void postAtTime(Runnable runnable, long uptimeMillis) {
mHandler.postAtTime(runnable, uptimeMillis);
}
};
SuggestionSession session = new SuggestionSession(
mSources, sources.mPromotableSources, sources.mUnpromotableSources,
mQueryExecutor,
mRefreshExecutor,
delayedExecutor, new SuggestionFactoryImpl(mContext),
SuggestionSession.CACHE_SUGGESTION_RESULTS);
session.setListener(this);
session.setShortcutRepo(mShortcutRepo);
return session;
}
private void warmUpWebSource(final SuggestionSource webSearchSource) {
mQueryExecutor.execute("warmup", new Runnable() {
public void run() {
try {
webSearchSource.getSuggestionTask("", 0, 0).call();
} catch (Exception e) {
Log.e(TAG, "exception from web search warm-up query", e);
}
}
});
}
/**
* Orders sources by source ranking. The ordering is as follows:
* - the web source is first regardless
* - the rest of the promoted sources are filled based on the ranking passed in
* - any unranked sources
* The above are put in mPromotableSources.
*
* The rest of the ranked sources are put in mUnpromotableSources.
*
* The idea is that unranked sources get a bump until they have enough data to be ranked like
* the rest, and at the same time, no source can be in the promoted list unless it has a high
* click through rate for a sustained amount of impressions.
*
* @param enabledSources The enabled sources.
* @param webSearchSource The name of the web search source, or <code>null</code> otherwise.
* @param sourceRanking The order the sources should be in.
* @param numPromoted The number of promoted sources.
*/
static Sources orderSources(
List<SuggestionSource> enabledSources,
SuggestionSource webSearchSource,
ArrayList<ComponentName> sourceRanking,
int numPromoted) {
// get any sources that are in the enabled sources in the order
final int numSources = enabledSources.size();
HashMap<ComponentName, SuggestionSource> linkMap =
new LinkedHashMap<ComponentName, SuggestionSource>(numSources);
for (int i = 0; i < numSources; i++) {
final SuggestionSource source = enabledSources.get(i);
linkMap.put(source.getComponentName(), source);
}
Sources sources = new Sources();
// gather set of ranked
final HashSet<ComponentName> allRanked = new HashSet<ComponentName>(sourceRanking);
// start with the web source if it exists
if (webSearchSource != null) {
if (DBG) Log.d(TAG, "Adding web search source: " + webSearchSource);
sources.add(webSearchSource, true);
}
// add ranked for rest of promoted slots
final int numRanked = sourceRanking.size();
int nextRanked = 0;
for (; nextRanked < numRanked && sources.mPromotableSources.size() < numPromoted;
nextRanked++) {
final ComponentName ranked = sourceRanking.get(nextRanked);
final SuggestionSource source = linkMap.remove(ranked);
if (DBG) Log.d(TAG, "Adding promoted ranked source: (" + ranked + ") " + source);
sources.add(source, true);
}
// now add the unranked
final Iterator<SuggestionSource> sourceIterator = linkMap.values().iterator();
while (sourceIterator.hasNext()) {
SuggestionSource source = sourceIterator.next();
if (!allRanked.contains(source.getComponentName())) {
if (DBG) Log.d(TAG, "Adding unranked source: " + source);
sources.add(source, false);
sourceIterator.remove();
}
}
// finally, add any remaining ranked to mUnpromotableSources
for (int i = nextRanked; i < numRanked; i++) {
final ComponentName ranked = sourceRanking.get(i);
final SuggestionSource source = linkMap.get(ranked);
if (DBG) Log.d(TAG, "Adding ranked source: (" + ranked + ") " + source);
sources.add(source, false);
}
if (DBG) Log.d(TAG, "Promotable sources: " + sources.mPromotableSources);
if (DBG) Log.d(TAG, "Unpromotable sources: " + sources.mUnpromotableSources);
return sources;
}
static class Sources {
public final ArrayList<SuggestionSource> mPromotableSources;
public final ArrayList<SuggestionSource> mUnpromotableSources;
public Sources() {
mPromotableSources = new ArrayList<SuggestionSource>();
mUnpromotableSources = new ArrayList<SuggestionSource>();
}
public void add(SuggestionSource source, boolean forcePromotable) {
if (source == null) return;
if (forcePromotable || shouldBePromotableWhenLowRanked(source)) {
if (DBG) Log.d(TAG, " Promotable: " + source);
mPromotableSources.add(source);
} else {
if (DBG) Log.d(TAG, " Unpromotable: " + source);
mUnpromotableSources.add(source);
}
}
}
private static boolean shouldBePromotableWhenLowRanked(SuggestionSource source) {
// TODO: this is an ugly hack to make sure the Music source is unpromotable unless
// it is ranked highly. (as long as there are at least
// SuggestionSession.NUM_PROMOTED_SOURCES other sources)
// Once the Music app returns better suggestions (i.e. token prefix matches, rather
// than string infix matches) this should be removed.
return !"com.android.music".equals(source.getComponentName().getPackageName());
}
}