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 17 package com.android.globalsearch; 18 19 import android.database.Cursor; 20 import android.content.Context; 21 import android.content.ComponentName; 22 import android.os.Handler; 23 import android.text.TextUtils; 24 import android.util.Log; 25 26 import java.util.concurrent.Executor; 27 import java.util.ArrayList; 28 import java.util.List; 29 import java.util.HashMap; 30 import java.util.LinkedHashMap; 31 import java.util.HashSet; 32 import java.util.Iterator; 33 34 /** 35 * Holds onto the current {@link SuggestionSession} and manages its lifecycle. When a session ends, 36 * it gets the session stats and reports them to the {@link ShortcutRepository}. 37 */ 38 public class SessionManager implements SuggestionSession.SessionCallback { 39 40 private static final String TAG = "SessionManager"; 41 private static final boolean DBG = false; 42 private static SessionManager sInstance; 43 44 private final Context mContext; 45 getInstance()46 public static synchronized SessionManager getInstance() { 47 return sInstance; 48 } 49 50 /** 51 * Refreshes the global session manager. 52 * 53 * @param sources The suggestion sources. 54 * @param shortcutRepo The shortcut repository. 55 * @param queryExecutor The executor used to execute search suggestion tasks. 56 * @param refreshExecutor The executor used execute shortcut refresh tasks. 57 * @param handler The handler passed along to the session. 58 * @return The up to date session manager. 59 */ refreshSessionmanager(Context context, SuggestionSources sources, ShortcutRepository shortcutRepo, PerTagExecutor queryExecutor, Executor refreshExecutor, Handler handler)60 public static synchronized SessionManager refreshSessionmanager(Context context, 61 SuggestionSources sources, ShortcutRepository shortcutRepo, 62 PerTagExecutor queryExecutor, 63 Executor refreshExecutor, Handler handler) { 64 if (DBG) Log.d(TAG, "refreshSessionmanager()"); 65 66 sInstance = new SessionManager(context, sources, shortcutRepo, 67 queryExecutor, refreshExecutor, handler); 68 return sInstance; 69 } 70 SessionManager(Context context, SuggestionSources sources, ShortcutRepository shortcutRepo, PerTagExecutor queryExecutor, Executor refreshExecutor, Handler handler)71 private SessionManager(Context context, 72 SuggestionSources sources, ShortcutRepository shortcutRepo, 73 PerTagExecutor queryExecutor, Executor refreshExecutor, Handler handler) { 74 mContext = context; 75 mSources = sources; 76 mShortcutRepo = shortcutRepo; 77 mQueryExecutor = queryExecutor; 78 mRefreshExecutor = refreshExecutor; 79 mHandler = handler; 80 } 81 82 private final SuggestionSources mSources; 83 private final ShortcutRepository mShortcutRepo; 84 private final PerTagExecutor mQueryExecutor; 85 private final Executor mRefreshExecutor; 86 private final Handler mHandler; 87 private SuggestionSession mSession; 88 89 /** 90 * Queries the current session for results. 91 * 92 * @see SuggestionSession#query(String) 93 */ query(Context context, String query)94 public synchronized Cursor query(Context context, String query) { 95 // create a new session if there is none, 96 // or when starting a new typing session 97 if (mSession == null || TextUtils.isEmpty(query)) { 98 mSession = createSession(); 99 } 100 101 return mSession.query(query); 102 } 103 104 /** {@inheritDoc} */ closeSession()105 public synchronized void closeSession() { 106 if (DBG) Log.d(TAG, "closeSession()"); 107 mSession = null; 108 } 109 createSession()110 private SuggestionSession createSession() { 111 if (DBG) Log.d(TAG, "createSession()"); 112 final SuggestionSource webSearchSource = mSources.getSelectedWebSearchSource(); 113 114 // Fire off a warm-up query to the web search source, which that source can use for 115 // whatever it sees fit. For example, EnhancedGoogleSearchProvider uses this to 116 // determine whether a opt-in needs to be shown for use of location. 117 if (webSearchSource != null) { 118 warmUpWebSource(webSearchSource); 119 } 120 121 Sources sources = orderSources( 122 mSources.getEnabledSuggestionSources(), 123 mSources, 124 mShortcutRepo.getSourceRanking(), 125 SuggestionSession.NUM_PROMOTED_SOURCES); 126 127 // implement the delayed executor using the handler 128 final DelayedExecutor delayedExecutor = new DelayedExecutor() { 129 public void postDelayed(Runnable runnable, long delayMillis) { 130 mHandler.postDelayed(runnable, delayMillis); 131 } 132 133 public void postAtTime(Runnable runnable, long uptimeMillis) { 134 mHandler.postAtTime(runnable, uptimeMillis); 135 } 136 }; 137 138 SuggestionSession session = new SuggestionSession( 139 mSources, sources.mPromotableSources, sources.mUnpromotableSources, 140 mQueryExecutor, 141 mRefreshExecutor, 142 delayedExecutor, new SuggestionFactoryImpl(mContext), 143 SuggestionSession.CACHE_SUGGESTION_RESULTS); 144 session.setListener(this); 145 session.setShortcutRepo(mShortcutRepo); 146 return session; 147 } 148 warmUpWebSource(final SuggestionSource webSearchSource)149 private void warmUpWebSource(final SuggestionSource webSearchSource) { 150 mQueryExecutor.execute("warmup", new Runnable() { 151 public void run() { 152 try { 153 webSearchSource.getSuggestionTask("", 0, 0).call(); 154 } catch (Exception e) { 155 Log.e(TAG, "exception from web search warm-up query", e); 156 } 157 } 158 }); 159 } 160 161 /** 162 * Orders sources by source ranking, and into two groups: one that are candidates for the 163 * promoted list (mPromotableSources), and the other containing sources that should not be in 164 * the promoted list (mUnpromotableSources). 165 * 166 * The promotable list is as follows: 167 * - the web source 168 * - up to 'numPromoted' - 1 of the best ranked sources, among source for whom we have enough 169 * data (e.g are in the 'sourceRanking' list) 170 * 171 * The unpromotoable list is as follows: 172 * - the sources lacking any impression / click data 173 * - the rest of the ranked sources 174 * 175 * The idea is to have the best ranked sources in the promoted list, and give newer sources the 176 * best slots under the "more results" positions to get a little extra attention until we have 177 * enough data to rank them as usual. 178 * 179 * Finally, to solve the empty room problem when there is no data about any sources, we allow 180 * a for a small whitelist of known system apps to be in the promoted list when there is no other 181 * ranked source available. This should only take effect for the first few usages of 182 * Quick search box. 183 * 184 * @param enabledSources The enabled sources. 185 * @param sourceRanking The order the sources should be in. 186 * @param sourceLookup For getting the web search source and trusted sources. 187 * @param numPromoted The number of promoted sources. 188 * @return The order of the promotable and non-promotable sources. 189 */ orderSources( List<SuggestionSource> enabledSources, SourceLookup sourceLookup, ArrayList<ComponentName> sourceRanking, int numPromoted)190 static Sources orderSources( 191 List<SuggestionSource> enabledSources, 192 SourceLookup sourceLookup, 193 ArrayList<ComponentName> sourceRanking, 194 int numPromoted) { 195 196 // get any sources that are in the enabled sources in the order 197 final int numSources = enabledSources.size(); 198 HashMap<ComponentName, SuggestionSource> linkMap = 199 new LinkedHashMap<ComponentName, SuggestionSource>(numSources); 200 for (int i = 0; i < numSources; i++) { 201 final SuggestionSource source = enabledSources.get(i); 202 linkMap.put(source.getComponentName(), source); 203 } 204 205 Sources sources = new Sources(); 206 207 // gather set of ranked 208 final HashSet<ComponentName> allRanked = new HashSet<ComponentName>(sourceRanking); 209 210 // start with the web source if it exists 211 SuggestionSource webSearchSource = sourceLookup.getSelectedWebSearchSource(); 212 if (webSearchSource != null) { 213 if (DBG) Log.d(TAG, "Adding web search source: " + webSearchSource); 214 sources.add(webSearchSource, true); 215 } 216 217 // add ranked for rest of promoted slots 218 final int numRanked = sourceRanking.size(); 219 int nextRanked = 0; 220 for (; nextRanked < numRanked && sources.mPromotableSources.size() < numPromoted; 221 nextRanked++) { 222 final ComponentName ranked = sourceRanking.get(nextRanked); 223 final SuggestionSource source = linkMap.remove(ranked); 224 if (DBG) Log.d(TAG, "Adding promoted ranked source: (" + ranked + ") " + source); 225 sources.add(source, true); 226 } 227 228 // now add the unranked 229 final Iterator<SuggestionSource> sourceIterator = linkMap.values().iterator(); 230 while (sourceIterator.hasNext()) { 231 SuggestionSource source = sourceIterator.next(); 232 if (!allRanked.contains(source.getComponentName())) { 233 if (DBG) Log.d(TAG, "Adding unranked source: " + source); 234 // To fix the empty room problem, we allow a small set of system apps 235 // to start putting their results in the promoted list before we 236 // have enough data to pick the high ranking ones. 237 sources.add(source, sourceLookup.isTrustedSource(source)); 238 sourceIterator.remove(); 239 } 240 } 241 242 // finally, add any remaining ranked 243 for (int i = nextRanked; i < numRanked; i++) { 244 final ComponentName ranked = sourceRanking.get(i); 245 final SuggestionSource source = linkMap.get(ranked); 246 if (source != null) { 247 if (DBG) Log.d(TAG, "Adding ranked source: (" + ranked + ") " + source); 248 sources.add(source, sourceLookup.isTrustedSource(source)); 249 } 250 } 251 252 if (DBG) Log.d(TAG, "Promotable sources: " + sources.mPromotableSources); 253 if (DBG) Log.d(TAG, "Unpromotable sources: " + sources.mUnpromotableSources); 254 255 return sources; 256 } 257 258 static class Sources { 259 public final ArrayList<SuggestionSource> mPromotableSources; 260 public final ArrayList<SuggestionSource> mUnpromotableSources; Sources()261 public Sources() { 262 mPromotableSources = new ArrayList<SuggestionSource>(); 263 mUnpromotableSources = new ArrayList<SuggestionSource>(); 264 } add(SuggestionSource source, boolean forcePromotable)265 public void add(SuggestionSource source, boolean forcePromotable) { 266 if (source == null) return; 267 if (forcePromotable) { 268 if (DBG) Log.d(TAG, " Promotable: " + source); 269 mPromotableSources.add(source); 270 } else { 271 if (DBG) Log.d(TAG, " Unpromotable: " + source); 272 mUnpromotableSources.add(source); 273 } 274 } 275 } 276 } 277