blob: bc44521adbfcad6a5c0fbb68e507a39dfc671886 [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;
24import android.database.DataSetObserver;
25import android.os.AsyncTask;
26import android.os.Handler;
27import android.text.TextUtils;
28import android.util.Log;
29import android.util.Xml;
30
31import com.android.internal.content.PackageMonitor;
32
33import org.xmlpull.v1.XmlPullParser;
34import org.xmlpull.v1.XmlPullParserException;
35import org.xmlpull.v1.XmlSerializer;
36
37import java.io.FileInputStream;
38import java.io.FileNotFoundException;
39import java.io.FileOutputStream;
40import java.io.IOException;
41import java.math.BigDecimal;
42import java.util.ArrayList;
43import java.util.Collections;
44import java.util.HashMap;
45import java.util.LinkedHashSet;
46import java.util.List;
47import java.util.Map;
48import java.util.Set;
49
50/**
51 * <p>
52 * This class represents a data model for choosing a component for handing a
53 * given {@link Intent}. The model is responsible for querying the system for
54 * activities that can handle the given intent and order found activities
55 * based on historical data of previous choices. The historical data is stored
56 * in an application private file. If a client does not want to have persistent
57 * choice history the file can be omitted, thus the activities will be ordered
58 * based on historical usage for the current session.
59 * <p>
60 * </p>
61 * For each backing history file there is a singleton instance of this class. Thus,
62 * several clients that specify the same history file will share the same model. Note
63 * that if multiple clients are sharing the same model they should implement semantically
64 * equivalent functionality since setting the model intent will change the found
65 * activities and they may be inconsistent with the functionality of some of the clients.
66 * For example, choosing a share activity can be implemented by a single backing
67 * model and two different views for performing the selection. If however, one of the
68 * views is used for sharing but the other for importing, for example, then each
69 * view should be backed by a separate model.
70 * </p>
71 * <p>
72 * The way clients interact with this class is as follows:
73 * </p>
74 * <p>
75 * <pre>
76 * <code>
77 * // Get a model and set it to a couple of clients with semantically similar function.
78 * ActivityChooserModel dataModel =
79 * ActivityChooserModel.get(context, "task_specific_history_file_name.xml");
80 *
81 * ActivityChooserModelClient modelClient1 = getActivityChooserModelClient1();
82 * modelClient1.setActivityChooserModel(dataModel);
83 *
84 * ActivityChooserModelClient modelClient2 = getActivityChooserModelClient2();
85 * modelClient2.setActivityChooserModel(dataModel);
86 *
87 * // Set an intent to choose a an activity for.
88 * dataModel.setIntent(intent);
89 * <pre>
90 * <code>
91 * </p>
92 * <p>
93 * <strong>Note:</strong> This class is thread safe.
94 * </p>
95 *
96 * @hide
97 */
98public class ActivityChooserModel extends DataSetObservable {
99
100 /**
101 * Client that utilizes an {@link ActivityChooserModel}.
102 */
103 public interface ActivityChooserModelClient {
104
105 /**
106 * Sets the {@link ActivityChooserModel}.
107 *
108 * @param dataModel The model.
109 */
110 public void setActivityChooserModel(ActivityChooserModel dataModel);
111 }
112
113 /**
114 * Defines a sorter that is responsible for sorting the activities
115 * based on the provided historical choices and an intent.
116 */
117 public interface ActivitySorter {
118
119 /**
120 * Sorts the <code>activities</code> in descending order of relevance
121 * based on previous history and an intent.
122 *
123 * @param intent The {@link Intent}.
124 * @param activities Activities to be sorted.
125 * @param historicalRecords Historical records.
126 */
127 // This cannot be done by a simple comparator since an Activity weight
128 // is computed from history. Note that Activity implements Comparable.
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700129 public void sort(Intent intent, List<ActivityResolveInfo> activities,
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700130 List<HistoricalRecord> historicalRecords);
131 }
132
133 /**
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700134 * Listener for choosing an activity.
135 */
136 public interface OnChooseActivityListener {
137
138 /**
139 * Called when an activity has been chosen. The client can decide whether
140 * an activity can be chosen and if so the caller of
141 * {@link ActivityChooserModel#chooseActivity(int)} will receive and {@link Intent}
142 * for launching it.
143 * <p>
144 * <strong>Note:</strong> Modifying the intent is not permitted and
145 * any changes to the latter will be ignored.
146 * </p>
147 *
148 * @param host The listener's host model.
149 * @param intent The intent for launching the chosen activity.
150 * @return Whether the intent is handled and should not be delivered to clients.
151 *
152 * @see ActivityChooserModel#chooseActivity(int)
153 */
154 public boolean onChooseActivity(ActivityChooserModel host, Intent intent);
155 }
156
157 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700158 * Flag for selecting debug mode.
159 */
160 private static final boolean DEBUG = false;
161
162 /**
163 * Tag used for logging.
164 */
165 private static final String LOG_TAG = ActivityChooserModel.class.getSimpleName();
166
167 /**
168 * The root tag in the history file.
169 */
170 private static final String TAG_HISTORICAL_RECORDS = "historical-records";
171
172 /**
173 * The tag for a record in the history file.
174 */
175 private static final String TAG_HISTORICAL_RECORD = "historical-record";
176
177 /**
178 * Attribute for the activity.
179 */
180 private static final String ATTRIBUTE_ACTIVITY = "activity";
181
182 /**
183 * Attribute for the choice time.
184 */
185 private static final String ATTRIBUTE_TIME = "time";
186
187 /**
188 * Attribute for the choice weight.
189 */
190 private static final String ATTRIBUTE_WEIGHT = "weight";
191
192 /**
193 * The default name of the choice history file.
194 */
195 public static final String DEFAULT_HISTORY_FILE_NAME =
196 "activity_choser_model_history.xml";
197
198 /**
199 * The default maximal length of the choice history.
200 */
201 public static final int DEFAULT_HISTORY_MAX_LENGTH = 50;
202
203 /**
204 * The amount with which to inflate a chosen activity when set as default.
205 */
206 private static final int DEFAULT_ACTIVITY_INFLATION = 5;
207
208 /**
209 * Default weight for a choice record.
210 */
211 private static final float DEFAULT_HISTORICAL_RECORD_WEIGHT = 1.0f;
212
213 /**
214 * The extension of the history file.
215 */
216 private static final String HISTORY_FILE_EXTENSION = ".xml";
217
218 /**
219 * An invalid item index.
220 */
221 private static final int INVALID_INDEX = -1;
222
223 /**
224 * Lock to guard the model registry.
225 */
226 private static final Object sRegistryLock = new Object();
227
228 /**
229 * This the registry for data models.
230 */
231 private static final Map<String, ActivityChooserModel> sDataModelRegistry =
232 new HashMap<String, ActivityChooserModel>();
233
234 /**
235 * Lock for synchronizing on this instance.
236 */
237 private final Object mInstanceLock = new Object();
238
239 /**
240 * List of activities that can handle the current intent.
241 */
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700242 private final List<ActivityResolveInfo> mActivites = new ArrayList<ActivityResolveInfo>();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700243
244 /**
245 * List with historical choice records.
246 */
247 private final List<HistoricalRecord> mHistoricalRecords = new ArrayList<HistoricalRecord>();
248
249 /**
250 * Monitor for added and removed packages.
251 */
252 private final PackageMonitor mPackageMonitor = new DataModelPackageMonitor();
253
254 /**
255 * Context for accessing resources.
256 */
257 private final Context mContext;
258
259 /**
260 * The name of the history file that backs this model.
261 */
262 private final String mHistoryFileName;
263
264 /**
265 * The intent for which a activity is being chosen.
266 */
267 private Intent mIntent;
268
269 /**
270 * The sorter for ordering activities based on intent and past choices.
271 */
272 private ActivitySorter mActivitySorter = new DefaultSorter();
273
274 /**
275 * The maximal length of the choice history.
276 */
277 private int mHistoryMaxSize = DEFAULT_HISTORY_MAX_LENGTH;
278
279 /**
280 * Flag whether choice history can be read. In general many clients can
281 * share the same data model and {@link #readHistoricalData()} may be called
282 * by arbitrary of them any number of times. Therefore, this class guarantees
283 * that the very first read succeeds and subsequent reads can be performed
284 * only after a call to {@link #persistHistoricalData()} followed by change
285 * of the share records.
286 */
287 private boolean mCanReadHistoricalData = true;
288
289 /**
290 * Flag whether the choice history was read. This is used to enforce that
291 * before calling {@link #persistHistoricalData()} a call to
292 * {@link #persistHistoricalData()} has been made. This aims to avoid a
293 * scenario in which a choice history file exits, it is not read yet and
294 * it is overwritten. Note that always all historical records are read in
295 * full and the file is rewritten. This is necessary since we need to
296 * purge old records that are outside of the sliding window of past choices.
297 */
298 private boolean mReadShareHistoryCalled = false;
299
300 /**
301 * Flag whether the choice records have changed. In general many clients can
302 * share the same data model and {@link #persistHistoricalData()} may be called
303 * by arbitrary of them any number of times. Therefore, this class guarantees
304 * that choice history will be persisted only if it has changed.
305 */
306 private boolean mHistoricalRecordsChanged = true;
307
308 /**
309 * Hander for scheduling work on client tread.
310 */
311 private final Handler mHandler = new Handler();
312
313 /**
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700314 * Policy for controlling how the model handles chosen activities.
315 */
316 private OnChooseActivityListener mActivityChoserModelPolicy;
317
318 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700319 * Gets the data model backed by the contents of the provided file with historical data.
320 * Note that only one data model is backed by a given file, thus multiple calls with
321 * the same file name will return the same model instance. If no such instance is present
322 * it is created.
323 * <p>
324 * <strong>Note:</strong> To use the default historical data file clients should explicitly
325 * pass as file name {@link #DEFAULT_HISTORY_FILE_NAME}. If no persistence of the choice
326 * history is desired clients should pass <code>null</code> for the file name. In such
327 * case a new model is returned for each invocation.
328 * </p>
329 *
330 * <p>
331 * <strong>Always use difference historical data files for semantically different actions.
332 * For example, sharing is different from importing.</strong>
333 * </p>
334 *
335 * @param context Context for loading resources.
336 * @param historyFileName File name with choice history, <code>null</code>
337 * if the model should not be backed by a file. In this case the activities
338 * will be ordered only by data from the current session.
339 *
340 * @return The model.
341 */
342 public static ActivityChooserModel get(Context context, String historyFileName) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700343 synchronized (sRegistryLock) {
344 ActivityChooserModel dataModel = sDataModelRegistry.get(historyFileName);
345 if (dataModel == null) {
346 dataModel = new ActivityChooserModel(context, historyFileName);
347 sDataModelRegistry.put(historyFileName, dataModel);
348 }
Svetoslav Ganov8dbace22011-07-21 11:36:33 -0700349 dataModel.readHistoricalData();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700350 return dataModel;
351 }
352 }
353
354 /**
355 * Creates a new instance.
356 *
357 * @param context Context for loading resources.
358 * @param historyFileName The history XML file.
359 */
360 private ActivityChooserModel(Context context, String historyFileName) {
361 mContext = context.getApplicationContext();
362 if (!TextUtils.isEmpty(historyFileName)
363 && !historyFileName.endsWith(HISTORY_FILE_EXTENSION)) {
364 mHistoryFileName = historyFileName + HISTORY_FILE_EXTENSION;
365 } else {
366 mHistoryFileName = historyFileName;
367 }
368 mPackageMonitor.register(mContext, true);
369 }
370
371 /**
372 * Sets an intent for which to choose a activity.
373 * <p>
374 * <strong>Note:</strong> Clients must set only semantically similar
375 * intents for each data model.
376 * <p>
377 *
378 * @param intent The intent.
379 */
380 public void setIntent(Intent intent) {
381 synchronized (mInstanceLock) {
382 if (mIntent == intent) {
383 return;
384 }
385 mIntent = intent;
386 loadActivitiesLocked();
387 }
388 }
389
390 /**
391 * Gets the intent for which a activity is being chosen.
392 *
393 * @return The intent.
394 */
395 public Intent getIntent() {
396 synchronized (mInstanceLock) {
397 return mIntent;
398 }
399 }
400
401 /**
402 * Gets the number of activities that can handle the intent.
403 *
404 * @return The activity count.
405 *
406 * @see #setIntent(Intent)
407 */
408 public int getActivityCount() {
409 synchronized (mInstanceLock) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700410 return mActivites.size();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700411 }
412 }
413
414 /**
415 * Gets an activity at a given index.
416 *
417 * @return The activity.
418 *
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700419 * @see ActivityResolveInfo
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700420 * @see #setIntent(Intent)
421 */
422 public ResolveInfo getActivity(int index) {
423 synchronized (mInstanceLock) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700424 return mActivites.get(index).resolveInfo;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700425 }
426 }
427
428 /**
429 * Gets the index of a the given activity.
430 *
431 * @param activity The activity index.
432 *
433 * @return The index if found, -1 otherwise.
434 */
435 public int getActivityIndex(ResolveInfo activity) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700436 List<ActivityResolveInfo> activities = mActivites;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700437 final int activityCount = activities.size();
438 for (int i = 0; i < activityCount; i++) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700439 ActivityResolveInfo currentActivity = activities.get(i);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700440 if (currentActivity.resolveInfo == activity) {
441 return i;
442 }
443 }
444 return INVALID_INDEX;
445 }
446
447 /**
448 * Chooses a activity to handle the current intent. This will result in
449 * adding a historical record for that action and construct intent with
450 * its component name set such that it can be immediately started by the
451 * client.
452 * <p>
453 * <strong>Note:</strong> By calling this method the client guarantees
454 * that the returned intent will be started. This intent is returned to
455 * the client solely to let additional customization before the start.
456 * </p>
457 *
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700458 * @return An {@link Intent} for launching the activity or null if the
459 * policy has consumed the intent.
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700460 *
461 * @see HistoricalRecord
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700462 * @see OnChooseActivityListener
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700463 */
464 public Intent chooseActivity(int index) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700465 ActivityResolveInfo chosenActivity = mActivites.get(index);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700466
467 ComponentName chosenName = new ComponentName(
468 chosenActivity.resolveInfo.activityInfo.packageName,
469 chosenActivity.resolveInfo.activityInfo.name);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700470
471 Intent choiceIntent = new Intent(mIntent);
472 choiceIntent.setComponent(chosenName);
473
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700474 if (mActivityChoserModelPolicy != null) {
475 // Do not allow the policy to change the intent.
476 Intent choiceIntentCopy = new Intent(choiceIntent);
477 final boolean handled = mActivityChoserModelPolicy.onChooseActivity(this,
478 choiceIntentCopy);
479 if (handled) {
480 return null;
481 }
482 }
483
484 HistoricalRecord historicalRecord = new HistoricalRecord(chosenName,
485 System.currentTimeMillis(), DEFAULT_HISTORICAL_RECORD_WEIGHT);
486 addHisoricalRecord(historicalRecord);
487
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700488 return choiceIntent;
489 }
490
491 /**
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700492 * Sets the listener for choosing an activity.
493 *
494 * @param listener The listener.
495 */
496 public void setOnChooseActivityListener(OnChooseActivityListener listener) {
497 mActivityChoserModelPolicy = listener;
498 }
499
500 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700501 * Gets the default activity, The default activity is defined as the one
502 * with highest rank i.e. the first one in the list of activities that can
503 * handle the intent.
504 *
505 * @return The default activity, <code>null</code> id not activities.
506 *
507 * @see #getActivity(int)
508 */
509 public ResolveInfo getDefaultActivity() {
510 synchronized (mInstanceLock) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700511 if (!mActivites.isEmpty()) {
512 return mActivites.get(0).resolveInfo;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700513 }
514 }
515 return null;
516 }
517
518 /**
519 * Sets the default activity. The default activity is set by adding a
520 * historical record with weight high enough that this activity will
521 * become the highest ranked. Such a strategy guarantees that the default
522 * will eventually change if not used. Also the weight of the record for
523 * setting a default is inflated with a constant amount to guarantee that
524 * it will stay as default for awhile.
525 *
526 * @param index The index of the activity to set as default.
527 */
528 public void setDefaultActivity(int index) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700529 ActivityResolveInfo newDefaultActivity = mActivites.get(index);
530 ActivityResolveInfo oldDefaultActivity = mActivites.get(0);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700531
532 final float weight;
533 if (oldDefaultActivity != null) {
534 // Add a record with weight enough to boost the chosen at the top.
535 weight = oldDefaultActivity.weight - newDefaultActivity.weight
536 + DEFAULT_ACTIVITY_INFLATION;
537 } else {
538 weight = DEFAULT_HISTORICAL_RECORD_WEIGHT;
539 }
540
541 ComponentName defaultName = new ComponentName(
542 newDefaultActivity.resolveInfo.activityInfo.packageName,
543 newDefaultActivity.resolveInfo.activityInfo.name);
544 HistoricalRecord historicalRecord = new HistoricalRecord(defaultName,
545 System.currentTimeMillis(), weight);
546 addHisoricalRecord(historicalRecord);
547 }
548
549 /**
550 * Reads the history data from the backing file if the latter
551 * was provided. Calling this method more than once before a call
552 * to {@link #persistHistoricalData()} has been made has no effect.
553 * <p>
554 * <strong>Note:</strong> Historical data is read asynchronously and
555 * as soon as the reading is completed any registered
556 * {@link DataSetObserver}s will be notified. Also no historical
557 * data is read until this method is invoked.
558 * <p>
559 */
Svetoslav Ganov8dbace22011-07-21 11:36:33 -0700560 private void readHistoricalData() {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700561 synchronized (mInstanceLock) {
562 if (!mCanReadHistoricalData || !mHistoricalRecordsChanged) {
563 return;
564 }
565 mCanReadHistoricalData = false;
566 mReadShareHistoryCalled = true;
567 if (!TextUtils.isEmpty(mHistoryFileName)) {
568 AsyncTask.SERIAL_EXECUTOR.execute(new HistoryLoader());
569 }
570 }
571 }
572
573 /**
574 * Persists the history data to the backing file if the latter
575 * was provided. Calling this method before a call to {@link #readHistoricalData()}
576 * throws an exception. Calling this method more than one without choosing an
577 * activity has not effect.
578 *
579 * @throws IllegalStateException If this method is called before a call to
580 * {@link #readHistoricalData()}.
581 */
Svetoslav Ganov8dbace22011-07-21 11:36:33 -0700582 private void persistHistoricalData() {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700583 synchronized (mInstanceLock) {
584 if (!mReadShareHistoryCalled) {
585 throw new IllegalStateException("No preceding call to #readHistoricalData");
586 }
587 if (!mHistoricalRecordsChanged) {
588 return;
589 }
590 mHistoricalRecordsChanged = false;
591 mCanReadHistoricalData = true;
592 if (!TextUtils.isEmpty(mHistoryFileName)) {
593 AsyncTask.SERIAL_EXECUTOR.execute(new HistoryPersister());
594 }
595 }
596 }
597
598 /**
599 * Sets the sorter for ordering activities based on historical data and an intent.
600 *
601 * @param activitySorter The sorter.
602 *
603 * @see ActivitySorter
604 */
605 public void setActivitySorter(ActivitySorter activitySorter) {
606 synchronized (mInstanceLock) {
607 if (mActivitySorter == activitySorter) {
608 return;
609 }
610 mActivitySorter = activitySorter;
611 sortActivities();
612 }
613 }
614
615 /**
616 * Sorts the activities based on history and an intent. If
617 * a sorter is not specified this a default implementation is used.
618 *
619 * @see #setActivitySorter(ActivitySorter)
620 */
621 private void sortActivities() {
622 synchronized (mInstanceLock) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700623 if (mActivitySorter != null && !mActivites.isEmpty()) {
624 mActivitySorter.sort(mIntent, mActivites,
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700625 Collections.unmodifiableList(mHistoricalRecords));
626 notifyChanged();
627 }
628 }
629 }
630
631 /**
632 * Sets the maximal size of the historical data. Defaults to
633 * {@link #DEFAULT_HISTORY_MAX_LENGTH}
634 * <p>
635 * <strong>Note:</strong> Setting this property will immediately
636 * enforce the specified max history size by dropping enough old
637 * historical records to enforce the desired size. Thus, any
638 * records that exceed the history size will be discarded and
639 * irreversibly lost.
640 * </p>
641 *
642 * @param historyMaxSize The max history size.
643 */
644 public void setHistoryMaxSize(int historyMaxSize) {
645 synchronized (mInstanceLock) {
646 if (mHistoryMaxSize == historyMaxSize) {
647 return;
648 }
649 mHistoryMaxSize = historyMaxSize;
650 pruneExcessiveHistoricalRecordsLocked();
651 sortActivities();
652 }
653 }
654
655 /**
656 * Gets the history max size.
657 *
658 * @return The history max size.
659 */
660 public int getHistoryMaxSize() {
661 synchronized (mInstanceLock) {
662 return mHistoryMaxSize;
663 }
664 }
665
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700666 /**
667 * Gets the history size.
668 *
669 * @return The history size.
670 */
671 public int getHistorySize() {
672 synchronized (mInstanceLock) {
673 return mHistoricalRecords.size();
674 }
675 }
676
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700677 @Override
678 protected void finalize() throws Throwable {
679 super.finalize();
680 mPackageMonitor.unregister();
681 }
682
683 /**
684 * Adds a historical record.
685 *
686 * @param historicalRecord The record to add.
687 * @return True if the record was added.
688 */
689 private boolean addHisoricalRecord(HistoricalRecord historicalRecord) {
690 synchronized (mInstanceLock) {
691 final boolean added = mHistoricalRecords.add(historicalRecord);
692 if (added) {
693 mHistoricalRecordsChanged = true;
694 pruneExcessiveHistoricalRecordsLocked();
Svetoslav Ganov8dbace22011-07-21 11:36:33 -0700695 persistHistoricalData();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700696 sortActivities();
697 }
698 return added;
699 }
700 }
701
702 /**
703 * Prunes older excessive records to guarantee {@link #mHistoryMaxSize}.
704 */
705 private void pruneExcessiveHistoricalRecordsLocked() {
706 List<HistoricalRecord> choiceRecords = mHistoricalRecords;
707 final int pruneCount = choiceRecords.size() - mHistoryMaxSize;
708 if (pruneCount <= 0) {
709 return;
710 }
711 mHistoricalRecordsChanged = true;
712 for (int i = 0; i < pruneCount; i++) {
713 HistoricalRecord prunedRecord = choiceRecords.remove(0);
714 if (DEBUG) {
715 Log.i(LOG_TAG, "Pruned: " + prunedRecord);
716 }
717 }
718 }
719
720 /**
721 * Loads the activities.
722 */
723 private void loadActivitiesLocked() {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700724 mActivites.clear();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700725 if (mIntent != null) {
726 List<ResolveInfo> resolveInfos =
727 mContext.getPackageManager().queryIntentActivities(mIntent, 0);
728 final int resolveInfoCount = resolveInfos.size();
729 for (int i = 0; i < resolveInfoCount; i++) {
730 ResolveInfo resolveInfo = resolveInfos.get(i);
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700731 mActivites.add(new ActivityResolveInfo(resolveInfo));
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700732 }
733 sortActivities();
734 } else {
735 notifyChanged();
736 }
737 }
738
739 /**
740 * Prunes historical records for a package that goes away.
741 *
742 * @param packageName The name of the package that goes away.
743 */
744 private void pruneHistoricalRecordsForPackageLocked(String packageName) {
745 boolean recordsRemoved = false;
746
747 List<HistoricalRecord> historicalRecords = mHistoricalRecords;
748 for (int i = 0; i < historicalRecords.size(); i++) {
749 HistoricalRecord historicalRecord = historicalRecords.get(i);
750 String recordPackageName = historicalRecord.activity.getPackageName();
751 if (recordPackageName.equals(packageName)) {
752 historicalRecords.remove(historicalRecord);
753 recordsRemoved = true;
754 }
755 }
756
757 if (recordsRemoved) {
758 mHistoricalRecordsChanged = true;
759 sortActivities();
760 }
761 }
762
763 /**
764 * Represents a record in the history.
765 */
766 public final static class HistoricalRecord {
767
768 /**
769 * The activity name.
770 */
771 public final ComponentName activity;
772
773 /**
774 * The choice time.
775 */
776 public final long time;
777
778 /**
779 * The record weight.
780 */
781 public final float weight;
782
783 /**
784 * Creates a new instance.
785 *
786 * @param activityName The activity component name flattened to string.
787 * @param time The time the activity was chosen.
788 * @param weight The weight of the record.
789 */
790 public HistoricalRecord(String activityName, long time, float weight) {
791 this(ComponentName.unflattenFromString(activityName), time, weight);
792 }
793
794 /**
795 * Creates a new instance.
796 *
797 * @param activityName The activity name.
798 * @param time The time the activity was chosen.
799 * @param weight The weight of the record.
800 */
801 public HistoricalRecord(ComponentName activityName, long time, float weight) {
802 this.activity = activityName;
803 this.time = time;
804 this.weight = weight;
805 }
806
807 @Override
808 public int hashCode() {
809 final int prime = 31;
810 int result = 1;
811 result = prime * result + ((activity == null) ? 0 : activity.hashCode());
812 result = prime * result + (int) (time ^ (time >>> 32));
813 result = prime * result + Float.floatToIntBits(weight);
814 return result;
815 }
816
817 @Override
818 public boolean equals(Object obj) {
819 if (this == obj) {
820 return true;
821 }
822 if (obj == null) {
823 return false;
824 }
825 if (getClass() != obj.getClass()) {
826 return false;
827 }
828 HistoricalRecord other = (HistoricalRecord) obj;
829 if (activity == null) {
830 if (other.activity != null) {
831 return false;
832 }
833 } else if (!activity.equals(other.activity)) {
834 return false;
835 }
836 if (time != other.time) {
837 return false;
838 }
839 if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) {
840 return false;
841 }
842 return true;
843 }
844
845 @Override
846 public String toString() {
847 StringBuilder builder = new StringBuilder();
848 builder.append("[");
849 builder.append("; activity:").append(activity);
850 builder.append("; time:").append(time);
851 builder.append("; weight:").append(new BigDecimal(weight));
852 builder.append("]");
853 return builder.toString();
854 }
855 }
856
857 /**
858 * Represents an activity.
859 */
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700860 public final class ActivityResolveInfo implements Comparable<ActivityResolveInfo> {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700861
862 /**
863 * The {@link ResolveInfo} of the activity.
864 */
865 public final ResolveInfo resolveInfo;
866
867 /**
868 * Weight of the activity. Useful for sorting.
869 */
870 public float weight;
871
872 /**
873 * Creates a new instance.
874 *
875 * @param resolveInfo activity {@link ResolveInfo}.
876 */
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700877 public ActivityResolveInfo(ResolveInfo resolveInfo) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700878 this.resolveInfo = resolveInfo;
879 }
880
881 @Override
882 public int hashCode() {
883 return 31 + Float.floatToIntBits(weight);
884 }
885
886 @Override
887 public boolean equals(Object obj) {
888 if (this == obj) {
889 return true;
890 }
891 if (obj == null) {
892 return false;
893 }
894 if (getClass() != obj.getClass()) {
895 return false;
896 }
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700897 ActivityResolveInfo other = (ActivityResolveInfo) obj;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700898 if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) {
899 return false;
900 }
901 return true;
902 }
903
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700904 public int compareTo(ActivityResolveInfo another) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700905 return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight);
906 }
907
908 @Override
909 public String toString() {
910 StringBuilder builder = new StringBuilder();
911 builder.append("[");
912 builder.append("resolveInfo:").append(resolveInfo.toString());
913 builder.append("; weight:").append(new BigDecimal(weight));
914 builder.append("]");
915 return builder.toString();
916 }
917 }
918
919 /**
920 * Default activity sorter implementation.
921 */
922 private final class DefaultSorter implements ActivitySorter {
923 private static final float WEIGHT_DECAY_COEFFICIENT = 0.95f;
924
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700925 private final Map<String, ActivityResolveInfo> mPackageNameToActivityMap =
926 new HashMap<String, ActivityResolveInfo>();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700927
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700928 public void sort(Intent intent, List<ActivityResolveInfo> activities,
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700929 List<HistoricalRecord> historicalRecords) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700930 Map<String, ActivityResolveInfo> packageNameToActivityMap =
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700931 mPackageNameToActivityMap;
932 packageNameToActivityMap.clear();
933
934 final int activityCount = activities.size();
935 for (int i = 0; i < activityCount; i++) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700936 ActivityResolveInfo activity = activities.get(i);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700937 activity.weight = 0.0f;
938 String packageName = activity.resolveInfo.activityInfo.packageName;
939 packageNameToActivityMap.put(packageName, activity);
940 }
941
942 final int lastShareIndex = historicalRecords.size() - 1;
943 float nextRecordWeight = 1;
944 for (int i = lastShareIndex; i >= 0; i--) {
945 HistoricalRecord historicalRecord = historicalRecords.get(i);
946 String packageName = historicalRecord.activity.getPackageName();
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700947 ActivityResolveInfo activity = packageNameToActivityMap.get(packageName);
948 if (activity != null) {
949 activity.weight += historicalRecord.weight * nextRecordWeight;
950 nextRecordWeight = nextRecordWeight * WEIGHT_DECAY_COEFFICIENT;
951 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700952 }
953
954 Collections.sort(activities);
955
956 if (DEBUG) {
957 for (int i = 0; i < activityCount; i++) {
958 Log.i(LOG_TAG, "Sorted: " + activities.get(i));
959 }
960 }
961 }
962 }
963
964 /**
965 * Command for reading the historical records from a file off the UI thread.
966 */
967 private final class HistoryLoader implements Runnable {
968
969 public void run() {
970 FileInputStream fis = null;
971 try {
972 fis = mContext.openFileInput(mHistoryFileName);
973 } catch (FileNotFoundException fnfe) {
974 if (DEBUG) {
975 Log.i(LOG_TAG, "Could not open historical records file: " + mHistoryFileName);
976 }
977 return;
978 }
979 try {
980 XmlPullParser parser = Xml.newPullParser();
981 parser.setInput(fis, null);
982
983 int type = XmlPullParser.START_DOCUMENT;
984 while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) {
985 type = parser.next();
986 }
987
988 if (!TAG_HISTORICAL_RECORDS.equals(parser.getName())) {
989 throw new XmlPullParserException("Share records file does not start with "
990 + TAG_HISTORICAL_RECORDS + " tag.");
991 }
992
993 List<HistoricalRecord> readRecords = new ArrayList<HistoricalRecord>();
994
995 while (true) {
996 type = parser.next();
997 if (type == XmlPullParser.END_DOCUMENT) {
998 break;
999 }
1000 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
1001 continue;
1002 }
1003 String nodeName = parser.getName();
1004 if (!TAG_HISTORICAL_RECORD.equals(nodeName)) {
1005 throw new XmlPullParserException("Share records file not well-formed.");
1006 }
1007
1008 String activity = parser.getAttributeValue(null, ATTRIBUTE_ACTIVITY);
1009 final long time =
1010 Long.parseLong(parser.getAttributeValue(null, ATTRIBUTE_TIME));
1011 final float weight =
1012 Float.parseFloat(parser.getAttributeValue(null, ATTRIBUTE_WEIGHT));
1013
1014 HistoricalRecord readRecord = new HistoricalRecord(activity, time,
1015 weight);
1016 readRecords.add(readRecord);
1017
1018 if (DEBUG) {
1019 Log.i(LOG_TAG, "Read " + readRecord.toString());
1020 }
1021 }
1022
1023 if (DEBUG) {
1024 Log.i(LOG_TAG, "Read " + readRecords.size() + " historical records.");
1025 }
1026
1027 synchronized (mInstanceLock) {
1028 Set<HistoricalRecord> uniqueShareRecords =
1029 new LinkedHashSet<HistoricalRecord>(readRecords);
1030
1031 // Make sure no duplicates. Example: Read a file with
1032 // one record, add one record, persist the two records,
1033 // add a record, read the persisted records - the
1034 // read two records should not be added again.
1035 List<HistoricalRecord> historicalRecords = mHistoricalRecords;
1036 final int historicalRecordsCount = historicalRecords.size();
1037 for (int i = historicalRecordsCount - 1; i >= 0; i--) {
1038 HistoricalRecord historicalRecord = historicalRecords.get(i);
1039 uniqueShareRecords.add(historicalRecord);
1040 }
1041
1042 if (historicalRecords.size() == uniqueShareRecords.size()) {
1043 return;
1044 }
1045
1046 // Make sure the oldest records go to the end.
1047 historicalRecords.clear();
1048 historicalRecords.addAll(uniqueShareRecords);
1049
1050 mHistoricalRecordsChanged = true;
1051
1052 // Do this on the client thread since the client may be on the UI
1053 // thread, wait for data changes which happen during sorting, and
1054 // perform UI modification based on the data change.
1055 mHandler.post(new Runnable() {
1056 public void run() {
1057 pruneExcessiveHistoricalRecordsLocked();
1058 sortActivities();
1059 }
1060 });
1061 }
1062 } catch (XmlPullParserException xppe) {
1063 Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, xppe);
1064 } catch (IOException ioe) {
1065 Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, ioe);
1066 } finally {
1067 if (fis != null) {
1068 try {
1069 fis.close();
1070 } catch (IOException ioe) {
1071 /* ignore */
1072 }
1073 }
1074 }
1075 }
1076 }
1077
1078 /**
1079 * Command for persisting the historical records to a file off the UI thread.
1080 */
1081 private final class HistoryPersister implements Runnable {
1082
1083 public void run() {
1084 FileOutputStream fos = null;
1085 List<HistoricalRecord> records = null;
1086
1087 synchronized (mInstanceLock) {
1088 records = new ArrayList<HistoricalRecord>(mHistoricalRecords);
1089 }
1090
1091 try {
1092 fos = mContext.openFileOutput(mHistoryFileName, Context.MODE_PRIVATE);
1093 } catch (FileNotFoundException fnfe) {
1094 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, fnfe);
1095 return;
1096 }
1097
1098 XmlSerializer serializer = Xml.newSerializer();
1099
1100 try {
1101 serializer.setOutput(fos, null);
1102 serializer.startDocument("UTF-8", true);
1103 serializer.startTag(null, TAG_HISTORICAL_RECORDS);
1104
1105 final int recordCount = records.size();
1106 for (int i = 0; i < recordCount; i++) {
1107 HistoricalRecord record = records.remove(0);
1108 serializer.startTag(null, TAG_HISTORICAL_RECORD);
1109 serializer.attribute(null, ATTRIBUTE_ACTIVITY, record.activity.flattenToString());
1110 serializer.attribute(null, ATTRIBUTE_TIME, String.valueOf(record.time));
1111 serializer.attribute(null, ATTRIBUTE_WEIGHT, String.valueOf(record.weight));
1112 serializer.endTag(null, TAG_HISTORICAL_RECORD);
1113 if (DEBUG) {
1114 Log.i(LOG_TAG, "Wrote " + record.toString());
1115 }
1116 }
1117
1118 serializer.endTag(null, TAG_HISTORICAL_RECORDS);
1119 serializer.endDocument();
1120
1121 if (DEBUG) {
1122 Log.i(LOG_TAG, "Wrote " + recordCount + " historical records.");
1123 }
1124 } catch (IllegalArgumentException iae) {
1125 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, iae);
1126 } catch (IllegalStateException ise) {
1127 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ise);
1128 } catch (IOException ioe) {
1129 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ioe);
1130 } finally {
1131 if (fos != null) {
1132 try {
1133 fos.close();
1134 } catch (IOException e) {
1135 /* ignore */
1136 }
1137 }
1138 }
1139 }
1140 }
1141
1142 /**
1143 * Keeps in sync the historical records and activities with the installed applications.
1144 */
1145 private final class DataModelPackageMonitor extends PackageMonitor {
1146
1147 @Override
1148 public void onPackageAdded(String packageName, int uid) {
1149 synchronized (mInstanceLock) {
1150 loadActivitiesLocked();
1151 }
1152 }
1153
1154 @Override
1155 public void onPackageAppeared(String packageName, int reason) {
1156 synchronized (mInstanceLock) {
1157 loadActivitiesLocked();
1158 }
1159 }
1160
1161 @Override
1162 public void onPackageRemoved(String packageName, int uid) {
1163 synchronized (mInstanceLock) {
1164 pruneHistoricalRecordsForPackageLocked(packageName);
1165 loadActivitiesLocked();
1166 }
1167 }
1168
1169 @Override
1170 public void onPackageDisappeared(String packageName, int reason) {
1171 synchronized (mInstanceLock) {
1172 pruneHistoricalRecordsForPackageLocked(packageName);
1173 loadActivitiesLocked();
1174 }
1175 }
1176 }
1177}