blob: fe6c4f5477ac97002dc711dd6cf8ecf62c77ee5d [file] [log] [blame]
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001/*
2 * Copyright (C) 2011 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 android.widget;
18
19import android.content.ComponentName;
20import android.content.Context;
21import android.content.Intent;
22import android.content.pm.ResolveInfo;
23import android.database.DataSetObservable;
Svetoslav Ganovd57521c2012-05-07 18:39:07 -070024import android.database.DataSetObserver;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070025import android.os.AsyncTask;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070026import android.text.TextUtils;
27import android.util.Log;
28import android.util.Xml;
29
30import com.android.internal.content.PackageMonitor;
31
32import org.xmlpull.v1.XmlPullParser;
33import org.xmlpull.v1.XmlPullParserException;
34import org.xmlpull.v1.XmlSerializer;
35
36import java.io.FileInputStream;
37import java.io.FileNotFoundException;
38import java.io.FileOutputStream;
39import java.io.IOException;
40import java.math.BigDecimal;
41import java.util.ArrayList;
42import java.util.Collections;
43import java.util.HashMap;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070044import java.util.List;
45import java.util.Map;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070046
47/**
48 * <p>
49 * This class represents a data model for choosing a component for handing a
50 * given {@link Intent}. The model is responsible for querying the system for
51 * activities that can handle the given intent and order found activities
52 * based on historical data of previous choices. The historical data is stored
53 * in an application private file. If a client does not want to have persistent
54 * choice history the file can be omitted, thus the activities will be ordered
55 * based on historical usage for the current session.
56 * <p>
57 * </p>
58 * For each backing history file there is a singleton instance of this class. Thus,
59 * several clients that specify the same history file will share the same model. Note
60 * that if multiple clients are sharing the same model they should implement semantically
61 * equivalent functionality since setting the model intent will change the found
62 * activities and they may be inconsistent with the functionality of some of the clients.
63 * For example, choosing a share activity can be implemented by a single backing
64 * model and two different views for performing the selection. If however, one of the
65 * views is used for sharing but the other for importing, for example, then each
66 * view should be backed by a separate model.
67 * </p>
68 * <p>
69 * The way clients interact with this class is as follows:
70 * </p>
71 * <p>
72 * <pre>
73 * <code>
74 * // Get a model and set it to a couple of clients with semantically similar function.
75 * ActivityChooserModel dataModel =
76 * ActivityChooserModel.get(context, "task_specific_history_file_name.xml");
77 *
78 * ActivityChooserModelClient modelClient1 = getActivityChooserModelClient1();
79 * modelClient1.setActivityChooserModel(dataModel);
80 *
81 * ActivityChooserModelClient modelClient2 = getActivityChooserModelClient2();
82 * modelClient2.setActivityChooserModel(dataModel);
83 *
84 * // Set an intent to choose a an activity for.
85 * dataModel.setIntent(intent);
86 * <pre>
87 * <code>
88 * </p>
89 * <p>
90 * <strong>Note:</strong> This class is thread safe.
91 * </p>
92 *
93 * @hide
94 */
95public class ActivityChooserModel extends DataSetObservable {
96
97 /**
98 * Client that utilizes an {@link ActivityChooserModel}.
99 */
100 public interface ActivityChooserModelClient {
101
102 /**
103 * Sets the {@link ActivityChooserModel}.
104 *
105 * @param dataModel The model.
106 */
107 public void setActivityChooserModel(ActivityChooserModel dataModel);
108 }
109
110 /**
111 * Defines a sorter that is responsible for sorting the activities
112 * based on the provided historical choices and an intent.
113 */
114 public interface ActivitySorter {
115
116 /**
117 * Sorts the <code>activities</code> in descending order of relevance
118 * based on previous history and an intent.
119 *
120 * @param intent The {@link Intent}.
121 * @param activities Activities to be sorted.
122 * @param historicalRecords Historical records.
123 */
124 // This cannot be done by a simple comparator since an Activity weight
125 // is computed from history. Note that Activity implements Comparable.
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700126 public void sort(Intent intent, List<ActivityResolveInfo> activities,
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700127 List<HistoricalRecord> historicalRecords);
128 }
129
130 /**
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700131 * Listener for choosing an activity.
132 */
133 public interface OnChooseActivityListener {
134
135 /**
136 * Called when an activity has been chosen. The client can decide whether
137 * an activity can be chosen and if so the caller of
138 * {@link ActivityChooserModel#chooseActivity(int)} will receive and {@link Intent}
139 * for launching it.
140 * <p>
141 * <strong>Note:</strong> Modifying the intent is not permitted and
142 * any changes to the latter will be ignored.
143 * </p>
144 *
145 * @param host The listener's host model.
146 * @param intent The intent for launching the chosen activity.
147 * @return Whether the intent is handled and should not be delivered to clients.
148 *
149 * @see ActivityChooserModel#chooseActivity(int)
150 */
151 public boolean onChooseActivity(ActivityChooserModel host, Intent intent);
152 }
153
154 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700155 * Flag for selecting debug mode.
156 */
157 private static final boolean DEBUG = false;
158
159 /**
160 * Tag used for logging.
161 */
162 private static final String LOG_TAG = ActivityChooserModel.class.getSimpleName();
163
164 /**
165 * The root tag in the history file.
166 */
167 private static final String TAG_HISTORICAL_RECORDS = "historical-records";
168
169 /**
170 * The tag for a record in the history file.
171 */
172 private static final String TAG_HISTORICAL_RECORD = "historical-record";
173
174 /**
175 * Attribute for the activity.
176 */
177 private static final String ATTRIBUTE_ACTIVITY = "activity";
178
179 /**
180 * Attribute for the choice time.
181 */
182 private static final String ATTRIBUTE_TIME = "time";
183
184 /**
185 * Attribute for the choice weight.
186 */
187 private static final String ATTRIBUTE_WEIGHT = "weight";
188
189 /**
190 * The default name of the choice history file.
191 */
192 public static final String DEFAULT_HISTORY_FILE_NAME =
193 "activity_choser_model_history.xml";
194
195 /**
196 * The default maximal length of the choice history.
197 */
198 public static final int DEFAULT_HISTORY_MAX_LENGTH = 50;
199
200 /**
201 * The amount with which to inflate a chosen activity when set as default.
202 */
203 private static final int DEFAULT_ACTIVITY_INFLATION = 5;
204
205 /**
206 * Default weight for a choice record.
207 */
208 private static final float DEFAULT_HISTORICAL_RECORD_WEIGHT = 1.0f;
209
210 /**
211 * The extension of the history file.
212 */
213 private static final String HISTORY_FILE_EXTENSION = ".xml";
214
215 /**
216 * An invalid item index.
217 */
218 private static final int INVALID_INDEX = -1;
219
220 /**
221 * Lock to guard the model registry.
222 */
223 private static final Object sRegistryLock = new Object();
224
225 /**
226 * This the registry for data models.
227 */
228 private static final Map<String, ActivityChooserModel> sDataModelRegistry =
229 new HashMap<String, ActivityChooserModel>();
230
231 /**
232 * Lock for synchronizing on this instance.
233 */
234 private final Object mInstanceLock = new Object();
235
236 /**
237 * List of activities that can handle the current intent.
238 */
Svetoslav Ganovca858792012-05-05 18:00:26 -0700239 private final List<ActivityResolveInfo> mActivities = new ArrayList<ActivityResolveInfo>();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700240
241 /**
242 * List with historical choice records.
243 */
244 private final List<HistoricalRecord> mHistoricalRecords = new ArrayList<HistoricalRecord>();
245
246 /**
247 * Monitor for added and removed packages.
248 */
249 private final PackageMonitor mPackageMonitor = new DataModelPackageMonitor();
250
251 /**
252 * Context for accessing resources.
253 */
254 private final Context mContext;
255
256 /**
257 * The name of the history file that backs this model.
258 */
259 private final String mHistoryFileName;
260
261 /**
262 * The intent for which a activity is being chosen.
263 */
264 private Intent mIntent;
265
266 /**
267 * The sorter for ordering activities based on intent and past choices.
268 */
269 private ActivitySorter mActivitySorter = new DefaultSorter();
270
271 /**
272 * The maximal length of the choice history.
273 */
274 private int mHistoryMaxSize = DEFAULT_HISTORY_MAX_LENGTH;
275
276 /**
277 * Flag whether choice history can be read. In general many clients can
Svetoslav Ganovca858792012-05-05 18:00:26 -0700278 * share the same data model and {@link #readHistoricalDataIfNeeded()} may be called
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700279 * by arbitrary of them any number of times. Therefore, this class guarantees
280 * that the very first read succeeds and subsequent reads can be performed
Svetoslav Ganovca858792012-05-05 18:00:26 -0700281 * only after a call to {@link #persistHistoricalDataIfNeeded()} followed by change
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700282 * of the share records.
283 */
284 private boolean mCanReadHistoricalData = true;
285
286 /**
287 * Flag whether the choice history was read. This is used to enforce that
Svetoslav Ganovca858792012-05-05 18:00:26 -0700288 * before calling {@link #persistHistoricalDataIfNeeded()} a call to
289 * {@link #persistHistoricalDataIfNeeded()} has been made. This aims to avoid a
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700290 * scenario in which a choice history file exits, it is not read yet and
291 * it is overwritten. Note that always all historical records are read in
292 * full and the file is rewritten. This is necessary since we need to
293 * purge old records that are outside of the sliding window of past choices.
294 */
295 private boolean mReadShareHistoryCalled = false;
296
297 /**
298 * Flag whether the choice records have changed. In general many clients can
Svetoslav Ganovca858792012-05-05 18:00:26 -0700299 * share the same data model and {@link #persistHistoricalDataIfNeeded()} may be called
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700300 * by arbitrary of them any number of times. Therefore, this class guarantees
301 * that choice history will be persisted only if it has changed.
302 */
303 private boolean mHistoricalRecordsChanged = true;
304
305 /**
Svetoslav Ganovca858792012-05-05 18:00:26 -0700306 * Flag whether to reload the activities for the current intent.
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700307 */
Svetoslav Ganovca858792012-05-05 18:00:26 -0700308 private boolean mReloadActivities = false;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700309
310 /**
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700311 * Policy for controlling how the model handles chosen activities.
312 */
313 private OnChooseActivityListener mActivityChoserModelPolicy;
314
315 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700316 * Gets the data model backed by the contents of the provided file with historical data.
317 * Note that only one data model is backed by a given file, thus multiple calls with
318 * the same file name will return the same model instance. If no such instance is present
319 * it is created.
320 * <p>
321 * <strong>Note:</strong> To use the default historical data file clients should explicitly
322 * pass as file name {@link #DEFAULT_HISTORY_FILE_NAME}. If no persistence of the choice
323 * history is desired clients should pass <code>null</code> for the file name. In such
324 * case a new model is returned for each invocation.
325 * </p>
326 *
327 * <p>
328 * <strong>Always use difference historical data files for semantically different actions.
329 * For example, sharing is different from importing.</strong>
330 * </p>
331 *
332 * @param context Context for loading resources.
333 * @param historyFileName File name with choice history, <code>null</code>
334 * if the model should not be backed by a file. In this case the activities
335 * will be ordered only by data from the current session.
336 *
337 * @return The model.
338 */
339 public static ActivityChooserModel get(Context context, String historyFileName) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700340 synchronized (sRegistryLock) {
341 ActivityChooserModel dataModel = sDataModelRegistry.get(historyFileName);
342 if (dataModel == null) {
343 dataModel = new ActivityChooserModel(context, historyFileName);
344 sDataModelRegistry.put(historyFileName, dataModel);
345 }
346 return dataModel;
347 }
348 }
349
350 /**
351 * Creates a new instance.
352 *
353 * @param context Context for loading resources.
354 * @param historyFileName The history XML file.
355 */
356 private ActivityChooserModel(Context context, String historyFileName) {
357 mContext = context.getApplicationContext();
358 if (!TextUtils.isEmpty(historyFileName)
359 && !historyFileName.endsWith(HISTORY_FILE_EXTENSION)) {
360 mHistoryFileName = historyFileName + HISTORY_FILE_EXTENSION;
361 } else {
362 mHistoryFileName = historyFileName;
363 }
Dianne Hackbornd0d75032012-04-19 23:12:09 -0700364 mPackageMonitor.register(mContext, null, true);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700365 }
366
367 /**
368 * Sets an intent for which to choose a activity.
369 * <p>
370 * <strong>Note:</strong> Clients must set only semantically similar
371 * intents for each data model.
372 * <p>
373 *
374 * @param intent The intent.
375 */
376 public void setIntent(Intent intent) {
377 synchronized (mInstanceLock) {
378 if (mIntent == intent) {
379 return;
380 }
381 mIntent = intent;
Svetoslav Ganovca858792012-05-05 18:00:26 -0700382 mReloadActivities = true;
383 ensureConsistentState();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700384 }
385 }
386
387 /**
388 * Gets the intent for which a activity is being chosen.
389 *
390 * @return The intent.
391 */
392 public Intent getIntent() {
393 synchronized (mInstanceLock) {
394 return mIntent;
395 }
396 }
397
398 /**
399 * Gets the number of activities that can handle the intent.
400 *
401 * @return The activity count.
402 *
403 * @see #setIntent(Intent)
404 */
405 public int getActivityCount() {
406 synchronized (mInstanceLock) {
Svetoslav Ganovca858792012-05-05 18:00:26 -0700407 ensureConsistentState();
408 return mActivities.size();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700409 }
410 }
411
412 /**
413 * Gets an activity at a given index.
414 *
415 * @return The activity.
416 *
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700417 * @see ActivityResolveInfo
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700418 * @see #setIntent(Intent)
419 */
420 public ResolveInfo getActivity(int index) {
421 synchronized (mInstanceLock) {
Svetoslav Ganovca858792012-05-05 18:00:26 -0700422 ensureConsistentState();
423 return mActivities.get(index).resolveInfo;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700424 }
425 }
426
427 /**
428 * Gets the index of a the given activity.
429 *
430 * @param activity The activity index.
431 *
432 * @return The index if found, -1 otherwise.
433 */
434 public int getActivityIndex(ResolveInfo activity) {
Svetoslav Ganovca858792012-05-05 18:00:26 -0700435 synchronized (mInstanceLock) {
436 ensureConsistentState();
437 List<ActivityResolveInfo> activities = mActivities;
438 final int activityCount = activities.size();
439 for (int i = 0; i < activityCount; i++) {
440 ActivityResolveInfo currentActivity = activities.get(i);
441 if (currentActivity.resolveInfo == activity) {
442 return i;
443 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700444 }
Svetoslav Ganovca858792012-05-05 18:00:26 -0700445 return INVALID_INDEX;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700446 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700447 }
448
449 /**
450 * Chooses a activity to handle the current intent. This will result in
451 * adding a historical record for that action and construct intent with
452 * its component name set such that it can be immediately started by the
453 * client.
454 * <p>
455 * <strong>Note:</strong> By calling this method the client guarantees
456 * that the returned intent will be started. This intent is returned to
457 * the client solely to let additional customization before the start.
458 * </p>
459 *
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700460 * @return An {@link Intent} for launching the activity or null if the
461 * policy has consumed the intent.
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700462 *
463 * @see HistoricalRecord
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700464 * @see OnChooseActivityListener
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700465 */
466 public Intent chooseActivity(int index) {
Svetoslav Ganovca858792012-05-05 18:00:26 -0700467 synchronized (mInstanceLock) {
468 ensureConsistentState();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700469
Svetoslav Ganovca858792012-05-05 18:00:26 -0700470 ActivityResolveInfo chosenActivity = mActivities.get(index);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700471
Svetoslav Ganovca858792012-05-05 18:00:26 -0700472 ComponentName chosenName = new ComponentName(
473 chosenActivity.resolveInfo.activityInfo.packageName,
474 chosenActivity.resolveInfo.activityInfo.name);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700475
Svetoslav Ganovca858792012-05-05 18:00:26 -0700476 Intent choiceIntent = new Intent(mIntent);
477 choiceIntent.setComponent(chosenName);
478
479 if (mActivityChoserModelPolicy != null) {
480 // Do not allow the policy to change the intent.
481 Intent choiceIntentCopy = new Intent(choiceIntent);
482 final boolean handled = mActivityChoserModelPolicy.onChooseActivity(this,
483 choiceIntentCopy);
484 if (handled) {
485 return null;
486 }
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700487 }
Svetoslav Ganovca858792012-05-05 18:00:26 -0700488
489 HistoricalRecord historicalRecord = new HistoricalRecord(chosenName,
490 System.currentTimeMillis(), DEFAULT_HISTORICAL_RECORD_WEIGHT);
491 addHisoricalRecord(historicalRecord);
492
493 return choiceIntent;
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700494 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700495 }
496
497 /**
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700498 * Sets the listener for choosing an activity.
499 *
500 * @param listener The listener.
501 */
502 public void setOnChooseActivityListener(OnChooseActivityListener listener) {
Svetoslav Ganovca858792012-05-05 18:00:26 -0700503 synchronized (mInstanceLock) {
504 mActivityChoserModelPolicy = listener;
505 }
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700506 }
507
508 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700509 * Gets the default activity, The default activity is defined as the one
510 * with highest rank i.e. the first one in the list of activities that can
511 * handle the intent.
512 *
513 * @return The default activity, <code>null</code> id not activities.
514 *
515 * @see #getActivity(int)
516 */
517 public ResolveInfo getDefaultActivity() {
518 synchronized (mInstanceLock) {
Svetoslav Ganovca858792012-05-05 18:00:26 -0700519 ensureConsistentState();
520 if (!mActivities.isEmpty()) {
521 return mActivities.get(0).resolveInfo;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700522 }
523 }
524 return null;
525 }
526
527 /**
528 * Sets the default activity. The default activity is set by adding a
529 * historical record with weight high enough that this activity will
530 * become the highest ranked. Such a strategy guarantees that the default
531 * will eventually change if not used. Also the weight of the record for
532 * setting a default is inflated with a constant amount to guarantee that
533 * it will stay as default for awhile.
534 *
535 * @param index The index of the activity to set as default.
536 */
537 public void setDefaultActivity(int index) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700538 synchronized (mInstanceLock) {
Svetoslav Ganovca858792012-05-05 18:00:26 -0700539 ensureConsistentState();
540
541 ActivityResolveInfo newDefaultActivity = mActivities.get(index);
542 ActivityResolveInfo oldDefaultActivity = mActivities.get(0);
543
544 final float weight;
545 if (oldDefaultActivity != null) {
546 // Add a record with weight enough to boost the chosen at the top.
547 weight = oldDefaultActivity.weight - newDefaultActivity.weight
548 + DEFAULT_ACTIVITY_INFLATION;
549 } else {
550 weight = DEFAULT_HISTORICAL_RECORD_WEIGHT;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700551 }
Svetoslav Ganovca858792012-05-05 18:00:26 -0700552
553 ComponentName defaultName = new ComponentName(
554 newDefaultActivity.resolveInfo.activityInfo.packageName,
555 newDefaultActivity.resolveInfo.activityInfo.name);
556 HistoricalRecord historicalRecord = new HistoricalRecord(defaultName,
557 System.currentTimeMillis(), weight);
558 addHisoricalRecord(historicalRecord);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700559 }
560 }
561
562 /**
563 * Persists the history data to the backing file if the latter
Svetoslav Ganovca858792012-05-05 18:00:26 -0700564 * was provided. Calling this method before a call to {@link #readHistoricalDataIfNeeded()}
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700565 * throws an exception. Calling this method more than one without choosing an
566 * activity has not effect.
567 *
568 * @throws IllegalStateException If this method is called before a call to
Svetoslav Ganovca858792012-05-05 18:00:26 -0700569 * {@link #readHistoricalDataIfNeeded()}.
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700570 */
Svetoslav Ganovca858792012-05-05 18:00:26 -0700571 private void persistHistoricalDataIfNeeded() {
572 if (!mReadShareHistoryCalled) {
573 throw new IllegalStateException("No preceding call to #readHistoricalData");
574 }
575 if (!mHistoricalRecordsChanged) {
576 return;
577 }
578 mHistoricalRecordsChanged = false;
579 if (!TextUtils.isEmpty(mHistoryFileName)) {
580 new PersistHistoryAsyncTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR,
581 new ArrayList<HistoricalRecord>(mHistoricalRecords), mHistoryFileName);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700582 }
583 }
584
585 /**
586 * Sets the sorter for ordering activities based on historical data and an intent.
587 *
588 * @param activitySorter The sorter.
589 *
590 * @see ActivitySorter
591 */
592 public void setActivitySorter(ActivitySorter activitySorter) {
593 synchronized (mInstanceLock) {
594 if (mActivitySorter == activitySorter) {
595 return;
596 }
597 mActivitySorter = activitySorter;
Svetoslav Ganovca858792012-05-05 18:00:26 -0700598 if (sortActivitiesIfNeeded()) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700599 notifyChanged();
600 }
601 }
602 }
603
604 /**
605 * Sets the maximal size of the historical data. Defaults to
606 * {@link #DEFAULT_HISTORY_MAX_LENGTH}
607 * <p>
608 * <strong>Note:</strong> Setting this property will immediately
609 * enforce the specified max history size by dropping enough old
610 * historical records to enforce the desired size. Thus, any
611 * records that exceed the history size will be discarded and
612 * irreversibly lost.
613 * </p>
614 *
615 * @param historyMaxSize The max history size.
616 */
617 public void setHistoryMaxSize(int historyMaxSize) {
618 synchronized (mInstanceLock) {
619 if (mHistoryMaxSize == historyMaxSize) {
620 return;
621 }
622 mHistoryMaxSize = historyMaxSize;
Svetoslav Ganovca858792012-05-05 18:00:26 -0700623 pruneExcessiveHistoricalRecordsIfNeeded();
624 if (sortActivitiesIfNeeded()) {
625 notifyChanged();
626 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700627 }
628 }
629
630 /**
631 * Gets the history max size.
632 *
633 * @return The history max size.
634 */
635 public int getHistoryMaxSize() {
636 synchronized (mInstanceLock) {
637 return mHistoryMaxSize;
638 }
639 }
640
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700641 /**
642 * Gets the history size.
643 *
644 * @return The history size.
645 */
646 public int getHistorySize() {
647 synchronized (mInstanceLock) {
Svetoslav Ganovca858792012-05-05 18:00:26 -0700648 ensureConsistentState();
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700649 return mHistoricalRecords.size();
650 }
651 }
652
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700653 @Override
654 protected void finalize() throws Throwable {
655 super.finalize();
656 mPackageMonitor.unregister();
657 }
658
659 /**
Svetoslav Ganovca858792012-05-05 18:00:26 -0700660 * Ensures the model is in a consistent state which is the
661 * activities for the current intent have been loaded, the
662 * most recent history has been read, and the activities
663 * are sorted.
664 */
665 private void ensureConsistentState() {
666 boolean stateChanged = loadActivitiesIfNeeded();
667 stateChanged |= readHistoricalDataIfNeeded();
668 pruneExcessiveHistoricalRecordsIfNeeded();
669 if (stateChanged) {
670 sortActivitiesIfNeeded();
671 notifyChanged();
672 }
673 }
674
675 /**
676 * Sorts the activities if necessary which is if there is a
677 * sorter, there are some activities to sort, and there is some
678 * historical data.
679 *
680 * @return Whether sorting was performed.
681 */
682 private boolean sortActivitiesIfNeeded() {
683 if (mActivitySorter != null && mIntent != null
684 && !mActivities.isEmpty() && !mHistoricalRecords.isEmpty()) {
685 mActivitySorter.sort(mIntent, mActivities,
686 Collections.unmodifiableList(mHistoricalRecords));
687 return true;
688 }
689 return false;
690 }
691
692 /**
693 * Loads the activities for the current intent if needed which is
694 * if they are not already loaded for the current intent.
695 *
696 * @return Whether loading was performed.
697 */
698 private boolean loadActivitiesIfNeeded() {
699 if (mReloadActivities && mIntent != null) {
700 mReloadActivities = false;
701 mActivities.clear();
702 List<ResolveInfo> resolveInfos = mContext.getPackageManager()
703 .queryIntentActivities(mIntent, 0);
704 final int resolveInfoCount = resolveInfos.size();
705 for (int i = 0; i < resolveInfoCount; i++) {
706 ResolveInfo resolveInfo = resolveInfos.get(i);
707 mActivities.add(new ActivityResolveInfo(resolveInfo));
708 }
709 return true;
710 }
711 return false;
712 }
713
714 /**
715 * Reads the historical data if necessary which is it has
716 * changed, there is a history file, and there is not persist
717 * in progress.
718 *
719 * @return Whether reading was performed.
720 */
721 private boolean readHistoricalDataIfNeeded() {
722 if (mCanReadHistoricalData && mHistoricalRecordsChanged &&
723 !TextUtils.isEmpty(mHistoryFileName)) {
724 mCanReadHistoricalData = false;
725 mReadShareHistoryCalled = true;
726 readHistoricalDataImpl();
727 return true;
728 }
729 return false;
730 }
731
732 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700733 * Adds a historical record.
734 *
735 * @param historicalRecord The record to add.
736 * @return True if the record was added.
737 */
738 private boolean addHisoricalRecord(HistoricalRecord historicalRecord) {
Svetoslav Ganovca858792012-05-05 18:00:26 -0700739 final boolean added = mHistoricalRecords.add(historicalRecord);
740 if (added) {
741 mHistoricalRecordsChanged = true;
742 pruneExcessiveHistoricalRecordsIfNeeded();
743 persistHistoricalDataIfNeeded();
744 sortActivitiesIfNeeded();
745 notifyChanged();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700746 }
Svetoslav Ganovca858792012-05-05 18:00:26 -0700747 return added;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700748 }
749
750 /**
Svetoslav Ganovca858792012-05-05 18:00:26 -0700751 * Prunes older excessive records to guarantee maxHistorySize.
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700752 */
Svetoslav Ganovca858792012-05-05 18:00:26 -0700753 private void pruneExcessiveHistoricalRecordsIfNeeded() {
754 final int pruneCount = mHistoricalRecords.size() - mHistoryMaxSize;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700755 if (pruneCount <= 0) {
756 return;
757 }
758 mHistoricalRecordsChanged = true;
759 for (int i = 0; i < pruneCount; i++) {
Svetoslav Ganovca858792012-05-05 18:00:26 -0700760 HistoricalRecord prunedRecord = mHistoricalRecords.remove(0);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700761 if (DEBUG) {
762 Log.i(LOG_TAG, "Pruned: " + prunedRecord);
763 }
764 }
765 }
766
767 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700768 * Represents a record in the history.
769 */
770 public final static class HistoricalRecord {
771
772 /**
773 * The activity name.
774 */
775 public final ComponentName activity;
776
777 /**
778 * The choice time.
779 */
780 public final long time;
781
782 /**
783 * The record weight.
784 */
785 public final float weight;
786
787 /**
788 * Creates a new instance.
789 *
790 * @param activityName The activity component name flattened to string.
791 * @param time The time the activity was chosen.
792 * @param weight The weight of the record.
793 */
794 public HistoricalRecord(String activityName, long time, float weight) {
795 this(ComponentName.unflattenFromString(activityName), time, weight);
796 }
797
798 /**
799 * Creates a new instance.
800 *
801 * @param activityName The activity name.
802 * @param time The time the activity was chosen.
803 * @param weight The weight of the record.
804 */
805 public HistoricalRecord(ComponentName activityName, long time, float weight) {
806 this.activity = activityName;
807 this.time = time;
808 this.weight = weight;
809 }
810
811 @Override
812 public int hashCode() {
813 final int prime = 31;
814 int result = 1;
815 result = prime * result + ((activity == null) ? 0 : activity.hashCode());
816 result = prime * result + (int) (time ^ (time >>> 32));
817 result = prime * result + Float.floatToIntBits(weight);
818 return result;
819 }
820
821 @Override
822 public boolean equals(Object obj) {
823 if (this == obj) {
824 return true;
825 }
826 if (obj == null) {
827 return false;
828 }
829 if (getClass() != obj.getClass()) {
830 return false;
831 }
832 HistoricalRecord other = (HistoricalRecord) obj;
833 if (activity == null) {
834 if (other.activity != null) {
835 return false;
836 }
837 } else if (!activity.equals(other.activity)) {
838 return false;
839 }
840 if (time != other.time) {
841 return false;
842 }
843 if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) {
844 return false;
845 }
846 return true;
847 }
848
849 @Override
850 public String toString() {
851 StringBuilder builder = new StringBuilder();
852 builder.append("[");
853 builder.append("; activity:").append(activity);
854 builder.append("; time:").append(time);
855 builder.append("; weight:").append(new BigDecimal(weight));
856 builder.append("]");
857 return builder.toString();
858 }
859 }
860
861 /**
862 * Represents an activity.
863 */
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700864 public final class ActivityResolveInfo implements Comparable<ActivityResolveInfo> {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700865
866 /**
867 * The {@link ResolveInfo} of the activity.
868 */
869 public final ResolveInfo resolveInfo;
870
871 /**
872 * Weight of the activity. Useful for sorting.
873 */
874 public float weight;
875
876 /**
877 * Creates a new instance.
878 *
879 * @param resolveInfo activity {@link ResolveInfo}.
880 */
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700881 public ActivityResolveInfo(ResolveInfo resolveInfo) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700882 this.resolveInfo = resolveInfo;
883 }
884
885 @Override
886 public int hashCode() {
887 return 31 + Float.floatToIntBits(weight);
888 }
889
890 @Override
891 public boolean equals(Object obj) {
892 if (this == obj) {
893 return true;
894 }
895 if (obj == null) {
896 return false;
897 }
898 if (getClass() != obj.getClass()) {
899 return false;
900 }
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700901 ActivityResolveInfo other = (ActivityResolveInfo) obj;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700902 if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) {
903 return false;
904 }
905 return true;
906 }
907
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700908 public int compareTo(ActivityResolveInfo another) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700909 return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight);
910 }
911
912 @Override
913 public String toString() {
914 StringBuilder builder = new StringBuilder();
915 builder.append("[");
916 builder.append("resolveInfo:").append(resolveInfo.toString());
917 builder.append("; weight:").append(new BigDecimal(weight));
918 builder.append("]");
919 return builder.toString();
920 }
921 }
922
923 /**
924 * Default activity sorter implementation.
925 */
926 private final class DefaultSorter implements ActivitySorter {
927 private static final float WEIGHT_DECAY_COEFFICIENT = 0.95f;
928
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700929 private final Map<String, ActivityResolveInfo> mPackageNameToActivityMap =
930 new HashMap<String, ActivityResolveInfo>();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700931
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700932 public void sort(Intent intent, List<ActivityResolveInfo> activities,
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700933 List<HistoricalRecord> historicalRecords) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700934 Map<String, ActivityResolveInfo> packageNameToActivityMap =
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700935 mPackageNameToActivityMap;
936 packageNameToActivityMap.clear();
937
938 final int activityCount = activities.size();
939 for (int i = 0; i < activityCount; i++) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700940 ActivityResolveInfo activity = activities.get(i);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700941 activity.weight = 0.0f;
942 String packageName = activity.resolveInfo.activityInfo.packageName;
943 packageNameToActivityMap.put(packageName, activity);
944 }
945
946 final int lastShareIndex = historicalRecords.size() - 1;
947 float nextRecordWeight = 1;
948 for (int i = lastShareIndex; i >= 0; i--) {
949 HistoricalRecord historicalRecord = historicalRecords.get(i);
950 String packageName = historicalRecord.activity.getPackageName();
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700951 ActivityResolveInfo activity = packageNameToActivityMap.get(packageName);
952 if (activity != null) {
953 activity.weight += historicalRecord.weight * nextRecordWeight;
954 nextRecordWeight = nextRecordWeight * WEIGHT_DECAY_COEFFICIENT;
955 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700956 }
957
958 Collections.sort(activities);
959
960 if (DEBUG) {
961 for (int i = 0; i < activityCount; i++) {
962 Log.i(LOG_TAG, "Sorted: " + activities.get(i));
963 }
964 }
965 }
966 }
967
968 /**
969 * Command for reading the historical records from a file off the UI thread.
970 */
Svetoslav Ganovca858792012-05-05 18:00:26 -0700971 private void readHistoricalDataImpl() {
972 FileInputStream fis = null;
973 try {
974 fis = mContext.openFileInput(mHistoryFileName);
975 } catch (FileNotFoundException fnfe) {
976 if (DEBUG) {
977 Log.i(LOG_TAG, "Could not open historical records file: " + mHistoryFileName);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700978 }
Svetoslav Ganovca858792012-05-05 18:00:26 -0700979 return;
980 }
981 try {
982 XmlPullParser parser = Xml.newPullParser();
983 parser.setInput(fis, null);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700984
Svetoslav Ganovca858792012-05-05 18:00:26 -0700985 int type = XmlPullParser.START_DOCUMENT;
986 while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) {
987 type = parser.next();
988 }
989
990 if (!TAG_HISTORICAL_RECORDS.equals(parser.getName())) {
991 throw new XmlPullParserException("Share records file does not start with "
992 + TAG_HISTORICAL_RECORDS + " tag.");
993 }
994
995 List<HistoricalRecord> historicalRecords = mHistoricalRecords;
996 historicalRecords.clear();
997
998 while (true) {
999 type = parser.next();
1000 if (type == XmlPullParser.END_DOCUMENT) {
1001 break;
1002 }
1003 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
1004 continue;
1005 }
1006 String nodeName = parser.getName();
1007 if (!TAG_HISTORICAL_RECORD.equals(nodeName)) {
1008 throw new XmlPullParserException("Share records file not well-formed.");
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001009 }
1010
Svetoslav Ganovca858792012-05-05 18:00:26 -07001011 String activity = parser.getAttributeValue(null, ATTRIBUTE_ACTIVITY);
1012 final long time =
1013 Long.parseLong(parser.getAttributeValue(null, ATTRIBUTE_TIME));
1014 final float weight =
1015 Float.parseFloat(parser.getAttributeValue(null, ATTRIBUTE_WEIGHT));
1016 HistoricalRecord readRecord = new HistoricalRecord(activity, time, weight);
1017 historicalRecords.add(readRecord);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001018
1019 if (DEBUG) {
Svetoslav Ganovca858792012-05-05 18:00:26 -07001020 Log.i(LOG_TAG, "Read " + readRecord.toString());
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001021 }
Svetoslav Ganovca858792012-05-05 18:00:26 -07001022 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001023
Svetoslav Ganovca858792012-05-05 18:00:26 -07001024 if (DEBUG) {
1025 Log.i(LOG_TAG, "Read " + historicalRecords.size() + " historical records.");
1026 }
1027 } catch (XmlPullParserException xppe) {
1028 Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, xppe);
1029 } catch (IOException ioe) {
1030 Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, ioe);
1031 } finally {
1032 if (fis != null) {
1033 try {
1034 fis.close();
1035 } catch (IOException ioe) {
1036 /* ignore */
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001037 }
1038 }
1039 }
1040 }
1041
1042 /**
1043 * Command for persisting the historical records to a file off the UI thread.
1044 */
Svetoslav Ganovca858792012-05-05 18:00:26 -07001045 private final class PersistHistoryAsyncTask extends AsyncTask<Object, Void, Void> {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001046
Svetoslav Ganovca858792012-05-05 18:00:26 -07001047 @Override
1048 @SuppressWarnings("unchecked")
1049 public Void doInBackground(Object... args) {
1050 List<HistoricalRecord> historicalRecords = (List<HistoricalRecord>) args[0];
1051 String hostoryFileName = (String) args[1];
1052
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001053 FileOutputStream fos = null;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001054
1055 try {
Svetoslav Ganovca858792012-05-05 18:00:26 -07001056 fos = mContext.openFileOutput(hostoryFileName, Context.MODE_PRIVATE);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001057 } catch (FileNotFoundException fnfe) {
Svetoslav Ganovca858792012-05-05 18:00:26 -07001058 Log.e(LOG_TAG, "Error writing historical recrod file: " + hostoryFileName, fnfe);
1059 return null;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001060 }
1061
1062 XmlSerializer serializer = Xml.newSerializer();
1063
1064 try {
1065 serializer.setOutput(fos, null);
1066 serializer.startDocument("UTF-8", true);
1067 serializer.startTag(null, TAG_HISTORICAL_RECORDS);
1068
Svetoslav Ganovca858792012-05-05 18:00:26 -07001069 final int recordCount = historicalRecords.size();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001070 for (int i = 0; i < recordCount; i++) {
Svetoslav Ganovca858792012-05-05 18:00:26 -07001071 HistoricalRecord record = historicalRecords.remove(0);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001072 serializer.startTag(null, TAG_HISTORICAL_RECORD);
Svetoslav Ganovca858792012-05-05 18:00:26 -07001073 serializer.attribute(null, ATTRIBUTE_ACTIVITY,
1074 record.activity.flattenToString());
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001075 serializer.attribute(null, ATTRIBUTE_TIME, String.valueOf(record.time));
1076 serializer.attribute(null, ATTRIBUTE_WEIGHT, String.valueOf(record.weight));
1077 serializer.endTag(null, TAG_HISTORICAL_RECORD);
1078 if (DEBUG) {
1079 Log.i(LOG_TAG, "Wrote " + record.toString());
1080 }
1081 }
1082
1083 serializer.endTag(null, TAG_HISTORICAL_RECORDS);
1084 serializer.endDocument();
1085
1086 if (DEBUG) {
1087 Log.i(LOG_TAG, "Wrote " + recordCount + " historical records.");
1088 }
1089 } catch (IllegalArgumentException iae) {
1090 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, iae);
1091 } catch (IllegalStateException ise) {
1092 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ise);
1093 } catch (IOException ioe) {
1094 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ioe);
1095 } finally {
Svetoslav Ganovca858792012-05-05 18:00:26 -07001096 mCanReadHistoricalData = true;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001097 if (fos != null) {
1098 try {
1099 fos.close();
1100 } catch (IOException e) {
1101 /* ignore */
1102 }
1103 }
1104 }
Svetoslav Ganovca858792012-05-05 18:00:26 -07001105 return null;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001106 }
1107 }
1108
1109 /**
1110 * Keeps in sync the historical records and activities with the installed applications.
1111 */
1112 private final class DataModelPackageMonitor extends PackageMonitor {
1113
1114 @Override
Svetoslav Ganovca858792012-05-05 18:00:26 -07001115 public void onSomePackagesChanged() {
1116 mReloadActivities = true;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001117 }
1118 }
1119}