d7f110fc62ecb42c644a4ee7193f9b91083da451
[pub/Android/ownCloud.git] / actionbarsherlock / src / com / actionbarsherlock / widget / ActivityChooserModel.java
1 /*
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
17 package com.actionbarsherlock.widget;
18
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.pm.ResolveInfo;
23 import android.database.DataSetObservable;
24 import android.os.Handler;
25 import android.text.TextUtils;
26 import android.util.Log;
27 import android.util.Xml;
28 import org.xmlpull.v1.XmlPullParser;
29 import org.xmlpull.v1.XmlPullParserException;
30 import org.xmlpull.v1.XmlSerializer;
31
32 import java.io.FileInputStream;
33 import java.io.FileNotFoundException;
34 import java.io.FileOutputStream;
35 import java.io.IOException;
36 import java.math.BigDecimal;
37 import java.util.ArrayList;
38 import java.util.Collections;
39 import java.util.HashMap;
40 import java.util.LinkedHashSet;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.Set;
44 import java.util.concurrent.Executor;
45 import java.util.concurrent.Executors;
46
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 */
95 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.
126 public void sort(Intent intent, List<ActivityResolveInfo> activities,
127 List<HistoricalRecord> historicalRecords);
128 }
129
130 /**
131 * 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 /**
155 * 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 */
239 private final List<ActivityResolveInfo> mActivites = new ArrayList<ActivityResolveInfo>();
240
241 /**
242 * List with historical choice records.
243 */
244 private final List<HistoricalRecord> mHistoricalRecords = new ArrayList<HistoricalRecord>();
245
246 /**
247 * Context for accessing resources.
248 */
249 private final Context mContext;
250
251 /**
252 * The name of the history file that backs this model.
253 */
254 private final String mHistoryFileName;
255
256 /**
257 * The intent for which a activity is being chosen.
258 */
259 private Intent mIntent;
260
261 /**
262 * The sorter for ordering activities based on intent and past choices.
263 */
264 private ActivitySorter mActivitySorter = new DefaultSorter();
265
266 /**
267 * The maximal length of the choice history.
268 */
269 private int mHistoryMaxSize = DEFAULT_HISTORY_MAX_LENGTH;
270
271 /**
272 * Flag whether choice history can be read. In general many clients can
273 * share the same data model and {@link #readHistoricalData()} may be called
274 * by arbitrary of them any number of times. Therefore, this class guarantees
275 * that the very first read succeeds and subsequent reads can be performed
276 * only after a call to {@link #persistHistoricalData()} followed by change
277 * of the share records.
278 */
279 private boolean mCanReadHistoricalData = true;
280
281 /**
282 * Flag whether the choice history was read. This is used to enforce that
283 * before calling {@link #persistHistoricalData()} a call to
284 * {@link #persistHistoricalData()} has been made. This aims to avoid a
285 * scenario in which a choice history file exits, it is not read yet and
286 * it is overwritten. Note that always all historical records are read in
287 * full and the file is rewritten. This is necessary since we need to
288 * purge old records that are outside of the sliding window of past choices.
289 */
290 private boolean mReadShareHistoryCalled = false;
291
292 /**
293 * Flag whether the choice records have changed. In general many clients can
294 * share the same data model and {@link #persistHistoricalData()} may be called
295 * by arbitrary of them any number of times. Therefore, this class guarantees
296 * that choice history will be persisted only if it has changed.
297 */
298 private boolean mHistoricalRecordsChanged = true;
299
300 /**
301 * Hander for scheduling work on client tread.
302 */
303 private final Handler mHandler = new Handler();
304
305 /**
306 * Policy for controlling how the model handles chosen activities.
307 */
308 private OnChooseActivityListener mActivityChoserModelPolicy;
309
310 /**
311 * Gets the data model backed by the contents of the provided file with historical data.
312 * Note that only one data model is backed by a given file, thus multiple calls with
313 * the same file name will return the same model instance. If no such instance is present
314 * it is created.
315 * <p>
316 * <strong>Note:</strong> To use the default historical data file clients should explicitly
317 * pass as file name {@link #DEFAULT_HISTORY_FILE_NAME}. If no persistence of the choice
318 * history is desired clients should pass <code>null</code> for the file name. In such
319 * case a new model is returned for each invocation.
320 * </p>
321 *
322 * <p>
323 * <strong>Always use difference historical data files for semantically different actions.
324 * For example, sharing is different from importing.</strong>
325 * </p>
326 *
327 * @param context Context for loading resources.
328 * @param historyFileName File name with choice history, <code>null</code>
329 * if the model should not be backed by a file. In this case the activities
330 * will be ordered only by data from the current session.
331 *
332 * @return The model.
333 */
334 public static ActivityChooserModel get(Context context, String historyFileName) {
335 synchronized (sRegistryLock) {
336 ActivityChooserModel dataModel = sDataModelRegistry.get(historyFileName);
337 if (dataModel == null) {
338 dataModel = new ActivityChooserModel(context, historyFileName);
339 sDataModelRegistry.put(historyFileName, dataModel);
340 }
341 dataModel.readHistoricalData();
342 return dataModel;
343 }
344 }
345
346 /**
347 * Creates a new instance.
348 *
349 * @param context Context for loading resources.
350 * @param historyFileName The history XML file.
351 */
352 private ActivityChooserModel(Context context, String historyFileName) {
353 mContext = context.getApplicationContext();
354 if (!TextUtils.isEmpty(historyFileName)
355 && !historyFileName.endsWith(HISTORY_FILE_EXTENSION)) {
356 mHistoryFileName = historyFileName + HISTORY_FILE_EXTENSION;
357 } else {
358 mHistoryFileName = historyFileName;
359 }
360 }
361
362 /**
363 * Sets an intent for which to choose a activity.
364 * <p>
365 * <strong>Note:</strong> Clients must set only semantically similar
366 * intents for each data model.
367 * <p>
368 *
369 * @param intent The intent.
370 */
371 public void setIntent(Intent intent) {
372 synchronized (mInstanceLock) {
373 if (mIntent == intent) {
374 return;
375 }
376 mIntent = intent;
377 loadActivitiesLocked();
378 }
379 }
380
381 /**
382 * Gets the intent for which a activity is being chosen.
383 *
384 * @return The intent.
385 */
386 public Intent getIntent() {
387 synchronized (mInstanceLock) {
388 return mIntent;
389 }
390 }
391
392 /**
393 * Gets the number of activities that can handle the intent.
394 *
395 * @return The activity count.
396 *
397 * @see #setIntent(Intent)
398 */
399 public int getActivityCount() {
400 synchronized (mInstanceLock) {
401 return mActivites.size();
402 }
403 }
404
405 /**
406 * Gets an activity at a given index.
407 *
408 * @return The activity.
409 *
410 * @see ActivityResolveInfo
411 * @see #setIntent(Intent)
412 */
413 public ResolveInfo getActivity(int index) {
414 synchronized (mInstanceLock) {
415 return mActivites.get(index).resolveInfo;
416 }
417 }
418
419 /**
420 * Gets the index of a the given activity.
421 *
422 * @param activity The activity index.
423 *
424 * @return The index if found, -1 otherwise.
425 */
426 public int getActivityIndex(ResolveInfo activity) {
427 List<ActivityResolveInfo> activities = mActivites;
428 final int activityCount = activities.size();
429 for (int i = 0; i < activityCount; i++) {
430 ActivityResolveInfo currentActivity = activities.get(i);
431 if (currentActivity.resolveInfo == activity) {
432 return i;
433 }
434 }
435 return INVALID_INDEX;
436 }
437
438 /**
439 * Chooses a activity to handle the current intent. This will result in
440 * adding a historical record for that action and construct intent with
441 * its component name set such that it can be immediately started by the
442 * client.
443 * <p>
444 * <strong>Note:</strong> By calling this method the client guarantees
445 * that the returned intent will be started. This intent is returned to
446 * the client solely to let additional customization before the start.
447 * </p>
448 *
449 * @return An {@link Intent} for launching the activity or null if the
450 * policy has consumed the intent.
451 *
452 * @see HistoricalRecord
453 * @see OnChooseActivityListener
454 */
455 public Intent chooseActivity(int index) {
456 ActivityResolveInfo chosenActivity = mActivites.get(index);
457
458 ComponentName chosenName = new ComponentName(
459 chosenActivity.resolveInfo.activityInfo.packageName,
460 chosenActivity.resolveInfo.activityInfo.name);
461
462 Intent choiceIntent = new Intent(mIntent);
463 choiceIntent.setComponent(chosenName);
464
465 if (mActivityChoserModelPolicy != null) {
466 // Do not allow the policy to change the intent.
467 Intent choiceIntentCopy = new Intent(choiceIntent);
468 final boolean handled = mActivityChoserModelPolicy.onChooseActivity(this,
469 choiceIntentCopy);
470 if (handled) {
471 return null;
472 }
473 }
474
475 HistoricalRecord historicalRecord = new HistoricalRecord(chosenName,
476 System.currentTimeMillis(), DEFAULT_HISTORICAL_RECORD_WEIGHT);
477 addHisoricalRecord(historicalRecord);
478
479 return choiceIntent;
480 }
481
482 /**
483 * Sets the listener for choosing an activity.
484 *
485 * @param listener The listener.
486 */
487 public void setOnChooseActivityListener(OnChooseActivityListener listener) {
488 mActivityChoserModelPolicy = listener;
489 }
490
491 /**
492 * Gets the default activity, The default activity is defined as the one
493 * with highest rank i.e. the first one in the list of activities that can
494 * handle the intent.
495 *
496 * @return The default activity, <code>null</code> id not activities.
497 *
498 * @see #getActivity(int)
499 */
500 public ResolveInfo getDefaultActivity() {
501 synchronized (mInstanceLock) {
502 if (!mActivites.isEmpty()) {
503 return mActivites.get(0).resolveInfo;
504 }
505 }
506 return null;
507 }
508
509 /**
510 * Sets the default activity. The default activity is set by adding a
511 * historical record with weight high enough that this activity will
512 * become the highest ranked. Such a strategy guarantees that the default
513 * will eventually change if not used. Also the weight of the record for
514 * setting a default is inflated with a constant amount to guarantee that
515 * it will stay as default for awhile.
516 *
517 * @param index The index of the activity to set as default.
518 */
519 public void setDefaultActivity(int index) {
520 ActivityResolveInfo newDefaultActivity = mActivites.get(index);
521 ActivityResolveInfo oldDefaultActivity = mActivites.get(0);
522
523 final float weight;
524 if (oldDefaultActivity != null) {
525 // Add a record with weight enough to boost the chosen at the top.
526 weight = oldDefaultActivity.weight - newDefaultActivity.weight
527 + DEFAULT_ACTIVITY_INFLATION;
528 } else {
529 weight = DEFAULT_HISTORICAL_RECORD_WEIGHT;
530 }
531
532 ComponentName defaultName = new ComponentName(
533 newDefaultActivity.resolveInfo.activityInfo.packageName,
534 newDefaultActivity.resolveInfo.activityInfo.name);
535 HistoricalRecord historicalRecord = new HistoricalRecord(defaultName,
536 System.currentTimeMillis(), weight);
537 addHisoricalRecord(historicalRecord);
538 }
539
540 /**
541 * Reads the history data from the backing file if the latter
542 * was provided. Calling this method more than once before a call
543 * to {@link #persistHistoricalData()} has been made has no effect.
544 * <p>
545 * <strong>Note:</strong> Historical data is read asynchronously and
546 * as soon as the reading is completed any registered
547 * {@link DataSetObserver}s will be notified. Also no historical
548 * data is read until this method is invoked.
549 * <p>
550 */
551 private void readHistoricalData() {
552 synchronized (mInstanceLock) {
553 if (!mCanReadHistoricalData || !mHistoricalRecordsChanged) {
554 return;
555 }
556 mCanReadHistoricalData = false;
557 mReadShareHistoryCalled = true;
558 if (!TextUtils.isEmpty(mHistoryFileName)) {
559 /*AsyncTask.*/SERIAL_EXECUTOR.execute(new HistoryLoader());
560 }
561 }
562 }
563
564 private static final Executor SERIAL_EXECUTOR = Executors.newSingleThreadExecutor();
565
566 /**
567 * Persists the history data to the backing file if the latter
568 * was provided. Calling this method before a call to {@link #readHistoricalData()}
569 * throws an exception. Calling this method more than one without choosing an
570 * activity has not effect.
571 *
572 * @throws IllegalStateException If this method is called before a call to
573 * {@link #readHistoricalData()}.
574 */
575 private void persistHistoricalData() {
576 synchronized (mInstanceLock) {
577 if (!mReadShareHistoryCalled) {
578 throw new IllegalStateException("No preceding call to #readHistoricalData");
579 }
580 if (!mHistoricalRecordsChanged) {
581 return;
582 }
583 mHistoricalRecordsChanged = false;
584 mCanReadHistoricalData = true;
585 if (!TextUtils.isEmpty(mHistoryFileName)) {
586 /*AsyncTask.*/SERIAL_EXECUTOR.execute(new HistoryPersister());
587 }
588 }
589 }
590
591 /**
592 * Sets the sorter for ordering activities based on historical data and an intent.
593 *
594 * @param activitySorter The sorter.
595 *
596 * @see ActivitySorter
597 */
598 public void setActivitySorter(ActivitySorter activitySorter) {
599 synchronized (mInstanceLock) {
600 if (mActivitySorter == activitySorter) {
601 return;
602 }
603 mActivitySorter = activitySorter;
604 sortActivities();
605 }
606 }
607
608 /**
609 * Sorts the activities based on history and an intent. If
610 * a sorter is not specified this a default implementation is used.
611 *
612 * @see #setActivitySorter(ActivitySorter)
613 */
614 private void sortActivities() {
615 synchronized (mInstanceLock) {
616 if (mActivitySorter != null && !mActivites.isEmpty()) {
617 mActivitySorter.sort(mIntent, mActivites,
618 Collections.unmodifiableList(mHistoricalRecords));
619 notifyChanged();
620 }
621 }
622 }
623
624 /**
625 * Sets the maximal size of the historical data. Defaults to
626 * {@link #DEFAULT_HISTORY_MAX_LENGTH}
627 * <p>
628 * <strong>Note:</strong> Setting this property will immediately
629 * enforce the specified max history size by dropping enough old
630 * historical records to enforce the desired size. Thus, any
631 * records that exceed the history size will be discarded and
632 * irreversibly lost.
633 * </p>
634 *
635 * @param historyMaxSize The max history size.
636 */
637 public void setHistoryMaxSize(int historyMaxSize) {
638 synchronized (mInstanceLock) {
639 if (mHistoryMaxSize == historyMaxSize) {
640 return;
641 }
642 mHistoryMaxSize = historyMaxSize;
643 pruneExcessiveHistoricalRecordsLocked();
644 sortActivities();
645 }
646 }
647
648 /**
649 * Gets the history max size.
650 *
651 * @return The history max size.
652 */
653 public int getHistoryMaxSize() {
654 synchronized (mInstanceLock) {
655 return mHistoryMaxSize;
656 }
657 }
658
659 /**
660 * Gets the history size.
661 *
662 * @return The history size.
663 */
664 public int getHistorySize() {
665 synchronized (mInstanceLock) {
666 return mHistoricalRecords.size();
667 }
668 }
669
670 /**
671 * Adds a historical record.
672 *
673 * @param historicalRecord The record to add.
674 * @return True if the record was added.
675 */
676 private boolean addHisoricalRecord(HistoricalRecord historicalRecord) {
677 synchronized (mInstanceLock) {
678 final boolean added = mHistoricalRecords.add(historicalRecord);
679 if (added) {
680 mHistoricalRecordsChanged = true;
681 pruneExcessiveHistoricalRecordsLocked();
682 persistHistoricalData();
683 sortActivities();
684 }
685 return added;
686 }
687 }
688
689 /**
690 * Prunes older excessive records to guarantee {@link #mHistoryMaxSize}.
691 */
692 private void pruneExcessiveHistoricalRecordsLocked() {
693 List<HistoricalRecord> choiceRecords = mHistoricalRecords;
694 final int pruneCount = choiceRecords.size() - mHistoryMaxSize;
695 if (pruneCount <= 0) {
696 return;
697 }
698 mHistoricalRecordsChanged = true;
699 for (int i = 0; i < pruneCount; i++) {
700 HistoricalRecord prunedRecord = choiceRecords.remove(0);
701 if (DEBUG) {
702 Log.i(LOG_TAG, "Pruned: " + prunedRecord);
703 }
704 }
705 }
706
707 /**
708 * Loads the activities.
709 */
710 private void loadActivitiesLocked() {
711 mActivites.clear();
712 if (mIntent != null) {
713 List<ResolveInfo> resolveInfos =
714 mContext.getPackageManager().queryIntentActivities(mIntent, 0);
715 final int resolveInfoCount = resolveInfos.size();
716 for (int i = 0; i < resolveInfoCount; i++) {
717 ResolveInfo resolveInfo = resolveInfos.get(i);
718 mActivites.add(new ActivityResolveInfo(resolveInfo));
719 }
720 sortActivities();
721 } else {
722 notifyChanged();
723 }
724 }
725
726 /**
727 * Represents a record in the history.
728 */
729 public final static class HistoricalRecord {
730
731 /**
732 * The activity name.
733 */
734 public final ComponentName activity;
735
736 /**
737 * The choice time.
738 */
739 public final long time;
740
741 /**
742 * The record weight.
743 */
744 public final float weight;
745
746 /**
747 * Creates a new instance.
748 *
749 * @param activityName The activity component name flattened to string.
750 * @param time The time the activity was chosen.
751 * @param weight The weight of the record.
752 */
753 public HistoricalRecord(String activityName, long time, float weight) {
754 this(ComponentName.unflattenFromString(activityName), time, weight);
755 }
756
757 /**
758 * Creates a new instance.
759 *
760 * @param activityName The activity name.
761 * @param time The time the activity was chosen.
762 * @param weight The weight of the record.
763 */
764 public HistoricalRecord(ComponentName activityName, long time, float weight) {
765 this.activity = activityName;
766 this.time = time;
767 this.weight = weight;
768 }
769
770 @Override
771 public int hashCode() {
772 final int prime = 31;
773 int result = 1;
774 result = prime * result + ((activity == null) ? 0 : activity.hashCode());
775 result = prime * result + (int) (time ^ (time >>> 32));
776 result = prime * result + Float.floatToIntBits(weight);
777 return result;
778 }
779
780 @Override
781 public boolean equals(Object obj) {
782 if (this == obj) {
783 return true;
784 }
785 if (obj == null) {
786 return false;
787 }
788 if (getClass() != obj.getClass()) {
789 return false;
790 }
791 HistoricalRecord other = (HistoricalRecord) obj;
792 if (activity == null) {
793 if (other.activity != null) {
794 return false;
795 }
796 } else if (!activity.equals(other.activity)) {
797 return false;
798 }
799 if (time != other.time) {
800 return false;
801 }
802 if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) {
803 return false;
804 }
805 return true;
806 }
807
808 @Override
809 public String toString() {
810 StringBuilder builder = new StringBuilder();
811 builder.append("[");
812 builder.append("; activity:").append(activity);
813 builder.append("; time:").append(time);
814 builder.append("; weight:").append(new BigDecimal(weight));
815 builder.append("]");
816 return builder.toString();
817 }
818 }
819
820 /**
821 * Represents an activity.
822 */
823 public final class ActivityResolveInfo implements Comparable<ActivityResolveInfo> {
824
825 /**
826 * The {@link ResolveInfo} of the activity.
827 */
828 public final ResolveInfo resolveInfo;
829
830 /**
831 * Weight of the activity. Useful for sorting.
832 */
833 public float weight;
834
835 /**
836 * Creates a new instance.
837 *
838 * @param resolveInfo activity {@link ResolveInfo}.
839 */
840 public ActivityResolveInfo(ResolveInfo resolveInfo) {
841 this.resolveInfo = resolveInfo;
842 }
843
844 @Override
845 public int hashCode() {
846 return 31 + Float.floatToIntBits(weight);
847 }
848
849 @Override
850 public boolean equals(Object obj) {
851 if (this == obj) {
852 return true;
853 }
854 if (obj == null) {
855 return false;
856 }
857 if (getClass() != obj.getClass()) {
858 return false;
859 }
860 ActivityResolveInfo other = (ActivityResolveInfo) obj;
861 if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) {
862 return false;
863 }
864 return true;
865 }
866
867 public int compareTo(ActivityResolveInfo another) {
868 return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight);
869 }
870
871 @Override
872 public String toString() {
873 StringBuilder builder = new StringBuilder();
874 builder.append("[");
875 builder.append("resolveInfo:").append(resolveInfo.toString());
876 builder.append("; weight:").append(new BigDecimal(weight));
877 builder.append("]");
878 return builder.toString();
879 }
880 }
881
882 /**
883 * Default activity sorter implementation.
884 */
885 private final class DefaultSorter implements ActivitySorter {
886 private static final float WEIGHT_DECAY_COEFFICIENT = 0.95f;
887
888 private final Map<String, ActivityResolveInfo> mPackageNameToActivityMap =
889 new HashMap<String, ActivityResolveInfo>();
890
891 public void sort(Intent intent, List<ActivityResolveInfo> activities,
892 List<HistoricalRecord> historicalRecords) {
893 Map<String, ActivityResolveInfo> packageNameToActivityMap =
894 mPackageNameToActivityMap;
895 packageNameToActivityMap.clear();
896
897 final int activityCount = activities.size();
898 for (int i = 0; i < activityCount; i++) {
899 ActivityResolveInfo activity = activities.get(i);
900 activity.weight = 0.0f;
901 String packageName = activity.resolveInfo.activityInfo.packageName;
902 packageNameToActivityMap.put(packageName, activity);
903 }
904
905 final int lastShareIndex = historicalRecords.size() - 1;
906 float nextRecordWeight = 1;
907 for (int i = lastShareIndex; i >= 0; i--) {
908 HistoricalRecord historicalRecord = historicalRecords.get(i);
909 String packageName = historicalRecord.activity.getPackageName();
910 ActivityResolveInfo activity = packageNameToActivityMap.get(packageName);
911 if (activity != null) {
912 activity.weight += historicalRecord.weight * nextRecordWeight;
913 nextRecordWeight = nextRecordWeight * WEIGHT_DECAY_COEFFICIENT;
914 }
915 }
916
917 Collections.sort(activities);
918
919 if (DEBUG) {
920 for (int i = 0; i < activityCount; i++) {
921 Log.i(LOG_TAG, "Sorted: " + activities.get(i));
922 }
923 }
924 }
925 }
926
927 /**
928 * Command for reading the historical records from a file off the UI thread.
929 */
930 private final class HistoryLoader implements Runnable {
931
932 public void run() {
933 FileInputStream fis = null;
934 try {
935 fis = mContext.openFileInput(mHistoryFileName);
936 } catch (FileNotFoundException fnfe) {
937 if (DEBUG) {
938 Log.i(LOG_TAG, "Could not open historical records file: " + mHistoryFileName);
939 }
940 return;
941 }
942 try {
943 XmlPullParser parser = Xml.newPullParser();
944 parser.setInput(fis, null);
945
946 int type = XmlPullParser.START_DOCUMENT;
947 while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) {
948 type = parser.next();
949 }
950
951 if (!TAG_HISTORICAL_RECORDS.equals(parser.getName())) {
952 throw new XmlPullParserException("Share records file does not start with "
953 + TAG_HISTORICAL_RECORDS + " tag.");
954 }
955
956 List<HistoricalRecord> readRecords = new ArrayList<HistoricalRecord>();
957
958 while (true) {
959 type = parser.next();
960 if (type == XmlPullParser.END_DOCUMENT) {
961 break;
962 }
963 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
964 continue;
965 }
966 String nodeName = parser.getName();
967 if (!TAG_HISTORICAL_RECORD.equals(nodeName)) {
968 throw new XmlPullParserException("Share records file not well-formed.");
969 }
970
971 String activity = parser.getAttributeValue(null, ATTRIBUTE_ACTIVITY);
972 final long time =
973 Long.parseLong(parser.getAttributeValue(null, ATTRIBUTE_TIME));
974 final float weight =
975 Float.parseFloat(parser.getAttributeValue(null, ATTRIBUTE_WEIGHT));
976
977 HistoricalRecord readRecord = new HistoricalRecord(activity, time,
978 weight);
979 readRecords.add(readRecord);
980
981 if (DEBUG) {
982 Log.i(LOG_TAG, "Read " + readRecord.toString());
983 }
984 }
985
986 if (DEBUG) {
987 Log.i(LOG_TAG, "Read " + readRecords.size() + " historical records.");
988 }
989
990 synchronized (mInstanceLock) {
991 Set<HistoricalRecord> uniqueShareRecords =
992 new LinkedHashSet<HistoricalRecord>(readRecords);
993
994 // Make sure no duplicates. Example: Read a file with
995 // one record, add one record, persist the two records,
996 // add a record, read the persisted records - the
997 // read two records should not be added again.
998 List<HistoricalRecord> historicalRecords = mHistoricalRecords;
999 final int historicalRecordsCount = historicalRecords.size();
1000 for (int i = historicalRecordsCount - 1; i >= 0; i--) {
1001 HistoricalRecord historicalRecord = historicalRecords.get(i);
1002 uniqueShareRecords.add(historicalRecord);
1003 }
1004
1005 if (historicalRecords.size() == uniqueShareRecords.size()) {
1006 return;
1007 }
1008
1009 // Make sure the oldest records go to the end.
1010 historicalRecords.clear();
1011 historicalRecords.addAll(uniqueShareRecords);
1012
1013 mHistoricalRecordsChanged = true;
1014
1015 // Do this on the client thread since the client may be on the UI
1016 // thread, wait for data changes which happen during sorting, and
1017 // perform UI modification based on the data change.
1018 mHandler.post(new Runnable() {
1019 public void run() {
1020 pruneExcessiveHistoricalRecordsLocked();
1021 sortActivities();
1022 }
1023 });
1024 }
1025 } catch (XmlPullParserException xppe) {
1026 Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, xppe);
1027 } catch (IOException ioe) {
1028 Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, ioe);
1029 } finally {
1030 if (fis != null) {
1031 try {
1032 fis.close();
1033 } catch (IOException ioe) {
1034 /* ignore */
1035 }
1036 }
1037 }
1038 }
1039 }
1040
1041 /**
1042 * Command for persisting the historical records to a file off the UI thread.
1043 */
1044 private final class HistoryPersister implements Runnable {
1045
1046 public void run() {
1047 FileOutputStream fos = null;
1048 List<HistoricalRecord> records = null;
1049
1050 synchronized (mInstanceLock) {
1051 records = new ArrayList<HistoricalRecord>(mHistoricalRecords);
1052 }
1053
1054 try {
1055 fos = mContext.openFileOutput(mHistoryFileName, Context.MODE_PRIVATE);
1056 } catch (FileNotFoundException fnfe) {
1057 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, fnfe);
1058 return;
1059 }
1060
1061 XmlSerializer serializer = Xml.newSerializer();
1062
1063 try {
1064 serializer.setOutput(fos, null);
1065 serializer.startDocument("UTF-8", true);
1066 serializer.startTag(null, TAG_HISTORICAL_RECORDS);
1067
1068 final int recordCount = records.size();
1069 for (int i = 0; i < recordCount; i++) {
1070 HistoricalRecord record = records.remove(0);
1071 serializer.startTag(null, TAG_HISTORICAL_RECORD);
1072 serializer.attribute(null, ATTRIBUTE_ACTIVITY, record.activity.flattenToString());
1073 serializer.attribute(null, ATTRIBUTE_TIME, String.valueOf(record.time));
1074 serializer.attribute(null, ATTRIBUTE_WEIGHT, String.valueOf(record.weight));
1075 serializer.endTag(null, TAG_HISTORICAL_RECORD);
1076 if (DEBUG) {
1077 Log.i(LOG_TAG, "Wrote " + record.toString());
1078 }
1079 }
1080
1081 serializer.endTag(null, TAG_HISTORICAL_RECORDS);
1082 serializer.endDocument();
1083
1084 if (DEBUG) {
1085 Log.i(LOG_TAG, "Wrote " + recordCount + " historical records.");
1086 }
1087 } catch (IllegalArgumentException iae) {
1088 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, iae);
1089 } catch (IllegalStateException ise) {
1090 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ise);
1091 } catch (IOException ioe) {
1092 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ioe);
1093 } finally {
1094 if (fos != null) {
1095 try {
1096 fos.close();
1097 } catch (IOException e) {
1098 /* ignore */
1099 }
1100 }
1101 }
1102 }
1103 }
1104 }