Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
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 */
17 /**
18 * Mozilla: Changing the package.
19 */
20 //package android.widget;
21 package org.mozilla.gecko.widget;
23 // Mozilla: New import
24 import org.mozilla.gecko.Distribution;
25 import org.mozilla.gecko.GeckoProfile;
26 import java.io.File;
28 import android.content.BroadcastReceiver;
29 import android.content.ComponentName;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.content.IntentFilter;
33 import android.content.pm.ResolveInfo;
34 import android.database.DataSetObservable;
35 import android.os.AsyncTask;
36 import android.text.TextUtils;
37 import android.util.Log;
38 import android.util.Xml;
40 /**
41 * Mozilla: Unused import.
42 */
43 //import com.android.internal.content.PackageMonitor;
45 import org.xmlpull.v1.XmlPullParser;
46 import org.xmlpull.v1.XmlPullParserException;
47 import org.xmlpull.v1.XmlSerializer;
49 import java.io.FileInputStream;
50 import java.io.FileNotFoundException;
51 import java.io.FileOutputStream;
52 import java.io.IOException;
53 import java.math.BigDecimal;
54 import java.util.ArrayList;
55 import java.util.Collections;
56 import java.util.HashMap;
57 import java.util.Iterator;
58 import java.util.List;
59 import java.util.Map;
61 /**
62 * <p>
63 * This class represents a data model for choosing a component for handing a
64 * given {@link Intent}. The model is responsible for querying the system for
65 * activities that can handle the given intent and order found activities
66 * based on historical data of previous choices. The historical data is stored
67 * in an application private file. If a client does not want to have persistent
68 * choice history the file can be omitted, thus the activities will be ordered
69 * based on historical usage for the current session.
70 * <p>
71 * </p>
72 * For each backing history file there is a singleton instance of this class. Thus,
73 * several clients that specify the same history file will share the same model. Note
74 * that if multiple clients are sharing the same model they should implement semantically
75 * equivalent functionality since setting the model intent will change the found
76 * activities and they may be inconsistent with the functionality of some of the clients.
77 * For example, choosing a share activity can be implemented by a single backing
78 * model and two different views for performing the selection. If however, one of the
79 * views is used for sharing but the other for importing, for example, then each
80 * view should be backed by a separate model.
81 * </p>
82 * <p>
83 * The way clients interact with this class is as follows:
84 * </p>
85 * <p>
86 * <pre>
87 * <code>
88 * // Get a model and set it to a couple of clients with semantically similar function.
89 * ActivityChooserModel dataModel =
90 * ActivityChooserModel.get(context, "task_specific_history_file_name.xml");
91 *
92 * ActivityChooserModelClient modelClient1 = getActivityChooserModelClient1();
93 * modelClient1.setActivityChooserModel(dataModel);
94 *
95 * ActivityChooserModelClient modelClient2 = getActivityChooserModelClient2();
96 * modelClient2.setActivityChooserModel(dataModel);
97 *
98 * // Set an intent to choose a an activity for.
99 * dataModel.setIntent(intent);
100 * <pre>
101 * <code>
102 * </p>
103 * <p>
104 * <strong>Note:</strong> This class is thread safe.
105 * </p>
106 *
107 * @hide
108 */
109 public class ActivityChooserModel extends DataSetObservable {
111 /**
112 * Client that utilizes an {@link ActivityChooserModel}.
113 */
114 public interface ActivityChooserModelClient {
116 /**
117 * Sets the {@link ActivityChooserModel}.
118 *
119 * @param dataModel The model.
120 */
121 public void setActivityChooserModel(ActivityChooserModel dataModel);
122 }
124 /**
125 * Defines a sorter that is responsible for sorting the activities
126 * based on the provided historical choices and an intent.
127 */
128 public interface ActivitySorter {
130 /**
131 * Sorts the <code>activities</code> in descending order of relevance
132 * based on previous history and an intent.
133 *
134 * @param intent The {@link Intent}.
135 * @param activities Activities to be sorted.
136 * @param historicalRecords Historical records.
137 */
138 // This cannot be done by a simple comparator since an Activity weight
139 // is computed from history. Note that Activity implements Comparable.
140 public void sort(Intent intent, List<ActivityResolveInfo> activities,
141 List<HistoricalRecord> historicalRecords);
142 }
144 /**
145 * Listener for choosing an activity.
146 */
147 public interface OnChooseActivityListener {
149 /**
150 * Called when an activity has been chosen. The client can decide whether
151 * an activity can be chosen and if so the caller of
152 * {@link ActivityChooserModel#chooseActivity(int)} will receive and {@link Intent}
153 * for launching it.
154 * <p>
155 * <strong>Note:</strong> Modifying the intent is not permitted and
156 * any changes to the latter will be ignored.
157 * </p>
158 *
159 * @param host The listener's host model.
160 * @param intent The intent for launching the chosen activity.
161 * @return Whether the intent is handled and should not be delivered to clients.
162 *
163 * @see ActivityChooserModel#chooseActivity(int)
164 */
165 public boolean onChooseActivity(ActivityChooserModel host, Intent intent);
166 }
168 /**
169 * Flag for selecting debug mode.
170 */
171 private static final boolean DEBUG = false;
173 /**
174 * Tag used for logging.
175 */
176 private static final String LOG_TAG = ActivityChooserModel.class.getSimpleName();
178 /**
179 * The root tag in the history file.
180 */
181 private static final String TAG_HISTORICAL_RECORDS = "historical-records";
183 /**
184 * The tag for a record in the history file.
185 */
186 private static final String TAG_HISTORICAL_RECORD = "historical-record";
188 /**
189 * Attribute for the activity.
190 */
191 private static final String ATTRIBUTE_ACTIVITY = "activity";
193 /**
194 * Attribute for the choice time.
195 */
196 private static final String ATTRIBUTE_TIME = "time";
198 /**
199 * Attribute for the choice weight.
200 */
201 private static final String ATTRIBUTE_WEIGHT = "weight";
203 /**
204 * The default name of the choice history file.
205 */
206 public static final String DEFAULT_HISTORY_FILE_NAME =
207 "activity_choser_model_history.xml";
209 /**
210 * The default maximal length of the choice history.
211 */
212 public static final int DEFAULT_HISTORY_MAX_LENGTH = 50;
214 /**
215 * The amount with which to inflate a chosen activity when set as default.
216 */
217 private static final int DEFAULT_ACTIVITY_INFLATION = 5;
219 /**
220 * Default weight for a choice record.
221 */
222 private static final float DEFAULT_HISTORICAL_RECORD_WEIGHT = 1.0f;
224 /**
225 * The extension of the history file.
226 */
227 private static final String HISTORY_FILE_EXTENSION = ".xml";
229 /**
230 * An invalid item index.
231 */
232 private static final int INVALID_INDEX = -1;
234 /**
235 * Lock to guard the model registry.
236 */
237 private static final Object sRegistryLock = new Object();
239 /**
240 * This the registry for data models.
241 */
242 private static final Map<String, ActivityChooserModel> sDataModelRegistry =
243 new HashMap<String, ActivityChooserModel>();
245 /**
246 * Lock for synchronizing on this instance.
247 */
248 private final Object mInstanceLock = new Object();
250 /**
251 * List of activities that can handle the current intent.
252 */
253 private final List<ActivityResolveInfo> mActivities = new ArrayList<ActivityResolveInfo>();
255 /**
256 * List with historical choice records.
257 */
258 private final List<HistoricalRecord> mHistoricalRecords = new ArrayList<HistoricalRecord>();
260 /**
261 * Monitor for added and removed packages.
262 */
263 /**
264 * Mozilla: Converted from a PackageMonitor to a DataModelPackageMonitor to avoid importing a new class.
265 */
266 private final DataModelPackageMonitor mPackageMonitor = new DataModelPackageMonitor();
268 /**
269 * Context for accessing resources.
270 */
271 private final Context mContext;
273 /**
274 * The name of the history file that backs this model.
275 */
276 private final String mHistoryFileName;
278 /**
279 * The intent for which a activity is being chosen.
280 */
281 private Intent mIntent;
283 /**
284 * The sorter for ordering activities based on intent and past choices.
285 */
286 private ActivitySorter mActivitySorter = new DefaultSorter();
288 /**
289 * The maximal length of the choice history.
290 */
291 private int mHistoryMaxSize = DEFAULT_HISTORY_MAX_LENGTH;
293 /**
294 * Flag whether choice history can be read. In general many clients can
295 * share the same data model and {@link #readHistoricalDataIfNeeded()} may be called
296 * by arbitrary of them any number of times. Therefore, this class guarantees
297 * that the very first read succeeds and subsequent reads can be performed
298 * only after a call to {@link #persistHistoricalDataIfNeeded()} followed by change
299 * of the share records.
300 */
301 private boolean mCanReadHistoricalData = true;
303 /**
304 * Flag whether the choice history was read. This is used to enforce that
305 * before calling {@link #persistHistoricalDataIfNeeded()} a call to
306 * {@link #persistHistoricalDataIfNeeded()} has been made. This aims to avoid a
307 * scenario in which a choice history file exits, it is not read yet and
308 * it is overwritten. Note that always all historical records are read in
309 * full and the file is rewritten. This is necessary since we need to
310 * purge old records that are outside of the sliding window of past choices.
311 */
312 private boolean mReadShareHistoryCalled = false;
314 /**
315 * Flag whether the choice records have changed. In general many clients can
316 * share the same data model and {@link #persistHistoricalDataIfNeeded()} may be called
317 * by arbitrary of them any number of times. Therefore, this class guarantees
318 * that choice history will be persisted only if it has changed.
319 */
320 private boolean mHistoricalRecordsChanged = true;
322 /**
323 * Flag whether to reload the activities for the current intent.
324 */
325 private boolean mReloadActivities = false;
327 /**
328 * Policy for controlling how the model handles chosen activities.
329 */
330 private OnChooseActivityListener mActivityChoserModelPolicy;
332 /**
333 * Gets the data model backed by the contents of the provided file with historical data.
334 * Note that only one data model is backed by a given file, thus multiple calls with
335 * the same file name will return the same model instance. If no such instance is present
336 * it is created.
337 * <p>
338 * <strong>Note:</strong> To use the default historical data file clients should explicitly
339 * pass as file name {@link #DEFAULT_HISTORY_FILE_NAME}. If no persistence of the choice
340 * history is desired clients should pass <code>null</code> for the file name. In such
341 * case a new model is returned for each invocation.
342 * </p>
343 *
344 * <p>
345 * <strong>Always use difference historical data files for semantically different actions.
346 * For example, sharing is different from importing.</strong>
347 * </p>
348 *
349 * @param context Context for loading resources.
350 * @param historyFileName File name with choice history, <code>null</code>
351 * if the model should not be backed by a file. In this case the activities
352 * will be ordered only by data from the current session.
353 *
354 * @return The model.
355 */
356 public static ActivityChooserModel get(Context context, String historyFileName) {
357 synchronized (sRegistryLock) {
358 ActivityChooserModel dataModel = sDataModelRegistry.get(historyFileName);
359 if (dataModel == null) {
360 dataModel = new ActivityChooserModel(context, historyFileName);
361 sDataModelRegistry.put(historyFileName, dataModel);
362 }
363 return dataModel;
364 }
365 }
367 /**
368 * Creates a new instance.
369 *
370 * @param context Context for loading resources.
371 * @param historyFileName The history XML file.
372 */
373 private ActivityChooserModel(Context context, String historyFileName) {
374 mContext = context.getApplicationContext();
375 if (!TextUtils.isEmpty(historyFileName)
376 && !historyFileName.endsWith(HISTORY_FILE_EXTENSION)) {
377 mHistoryFileName = historyFileName + HISTORY_FILE_EXTENSION;
378 } else {
379 mHistoryFileName = historyFileName;
380 }
382 /**
383 * Mozilla: Uses modified receiver
384 */
385 mPackageMonitor.register(mContext);
386 }
388 /**
389 * Sets an intent for which to choose a activity.
390 * <p>
391 * <strong>Note:</strong> Clients must set only semantically similar
392 * intents for each data model.
393 * <p>
394 *
395 * @param intent The intent.
396 */
397 public void setIntent(Intent intent) {
398 synchronized (mInstanceLock) {
399 if (mIntent == intent) {
400 return;
401 }
402 mIntent = intent;
403 mReloadActivities = true;
404 ensureConsistentState();
405 }
406 }
408 /**
409 * Gets the intent for which a activity is being chosen.
410 *
411 * @return The intent.
412 */
413 public Intent getIntent() {
414 synchronized (mInstanceLock) {
415 return mIntent;
416 }
417 }
419 /**
420 * Gets the number of activities that can handle the intent.
421 *
422 * @return The activity count.
423 *
424 * @see #setIntent(Intent)
425 */
426 public int getActivityCount() {
427 synchronized (mInstanceLock) {
428 ensureConsistentState();
429 return mActivities.size();
430 }
431 }
433 /**
434 * Gets an activity at a given index.
435 *
436 * @return The activity.
437 *
438 * @see ActivityResolveInfo
439 * @see #setIntent(Intent)
440 */
441 public ResolveInfo getActivity(int index) {
442 synchronized (mInstanceLock) {
443 ensureConsistentState();
444 return mActivities.get(index).resolveInfo;
445 }
446 }
448 /**
449 * Gets the index of a the given activity.
450 *
451 * @param activity The activity index.
452 *
453 * @return The index if found, -1 otherwise.
454 */
455 public int getActivityIndex(ResolveInfo activity) {
456 synchronized (mInstanceLock) {
457 ensureConsistentState();
458 List<ActivityResolveInfo> activities = mActivities;
459 final int activityCount = activities.size();
460 for (int i = 0; i < activityCount; i++) {
461 ActivityResolveInfo currentActivity = activities.get(i);
462 if (currentActivity.resolveInfo == activity) {
463 return i;
464 }
465 }
466 return INVALID_INDEX;
467 }
468 }
470 /**
471 * Chooses a activity to handle the current intent. This will result in
472 * adding a historical record for that action and construct intent with
473 * its component name set such that it can be immediately started by the
474 * client.
475 * <p>
476 * <strong>Note:</strong> By calling this method the client guarantees
477 * that the returned intent will be started. This intent is returned to
478 * the client solely to let additional customization before the start.
479 * </p>
480 *
481 * @return An {@link Intent} for launching the activity or null if the
482 * policy has consumed the intent or there is not current intent
483 * set via {@link #setIntent(Intent)}.
484 *
485 * @see HistoricalRecord
486 * @see OnChooseActivityListener
487 */
488 public Intent chooseActivity(int index) {
489 synchronized (mInstanceLock) {
490 if (mIntent == null) {
491 return null;
492 }
494 ensureConsistentState();
496 ActivityResolveInfo chosenActivity = mActivities.get(index);
498 ComponentName chosenName = new ComponentName(
499 chosenActivity.resolveInfo.activityInfo.packageName,
500 chosenActivity.resolveInfo.activityInfo.name);
502 Intent choiceIntent = new Intent(mIntent);
503 choiceIntent.setComponent(chosenName);
505 if (mActivityChoserModelPolicy != null) {
506 // Do not allow the policy to change the intent.
507 Intent choiceIntentCopy = new Intent(choiceIntent);
508 final boolean handled = mActivityChoserModelPolicy.onChooseActivity(this,
509 choiceIntentCopy);
510 if (handled) {
511 return null;
512 }
513 }
515 HistoricalRecord historicalRecord = new HistoricalRecord(chosenName,
516 System.currentTimeMillis(), DEFAULT_HISTORICAL_RECORD_WEIGHT);
517 addHistoricalRecord(historicalRecord);
519 return choiceIntent;
520 }
521 }
523 /**
524 * Sets the listener for choosing an activity.
525 *
526 * @param listener The listener.
527 */
528 public void setOnChooseActivityListener(OnChooseActivityListener listener) {
529 synchronized (mInstanceLock) {
530 mActivityChoserModelPolicy = listener;
531 }
532 }
534 /**
535 * Gets the default activity, The default activity is defined as the one
536 * with highest rank i.e. the first one in the list of activities that can
537 * handle the intent.
538 *
539 * @return The default activity, <code>null</code> id not activities.
540 *
541 * @see #getActivity(int)
542 */
543 public ResolveInfo getDefaultActivity() {
544 synchronized (mInstanceLock) {
545 ensureConsistentState();
546 if (!mActivities.isEmpty()) {
547 return mActivities.get(0).resolveInfo;
548 }
549 }
550 return null;
551 }
553 /**
554 * Sets the default activity. The default activity is set by adding a
555 * historical record with weight high enough that this activity will
556 * become the highest ranked. Such a strategy guarantees that the default
557 * will eventually change if not used. Also the weight of the record for
558 * setting a default is inflated with a constant amount to guarantee that
559 * it will stay as default for awhile.
560 *
561 * @param index The index of the activity to set as default.
562 */
563 public void setDefaultActivity(int index) {
564 synchronized (mInstanceLock) {
565 ensureConsistentState();
567 ActivityResolveInfo newDefaultActivity = mActivities.get(index);
568 ActivityResolveInfo oldDefaultActivity = mActivities.get(0);
570 final float weight;
571 if (oldDefaultActivity != null) {
572 // Add a record with weight enough to boost the chosen at the top.
573 weight = oldDefaultActivity.weight - newDefaultActivity.weight
574 + DEFAULT_ACTIVITY_INFLATION;
575 } else {
576 weight = DEFAULT_HISTORICAL_RECORD_WEIGHT;
577 }
579 ComponentName defaultName = new ComponentName(
580 newDefaultActivity.resolveInfo.activityInfo.packageName,
581 newDefaultActivity.resolveInfo.activityInfo.name);
582 HistoricalRecord historicalRecord = new HistoricalRecord(defaultName,
583 System.currentTimeMillis(), weight);
584 addHistoricalRecord(historicalRecord);
585 }
586 }
588 /**
589 * Persists the history data to the backing file if the latter
590 * was provided. Calling this method before a call to {@link #readHistoricalDataIfNeeded()}
591 * throws an exception. Calling this method more than one without choosing an
592 * activity has not effect.
593 *
594 * @throws IllegalStateException If this method is called before a call to
595 * {@link #readHistoricalDataIfNeeded()}.
596 */
597 private void persistHistoricalDataIfNeeded() {
598 if (!mReadShareHistoryCalled) {
599 throw new IllegalStateException("No preceding call to #readHistoricalData");
600 }
601 if (!mHistoricalRecordsChanged) {
602 return;
603 }
604 mHistoricalRecordsChanged = false;
605 if (!TextUtils.isEmpty(mHistoryFileName)) {
606 /**
607 * Mozilla: Converted to a normal task.execute call so that this works on < ICS phones.
608 */
609 new PersistHistoryAsyncTask().execute(new ArrayList<HistoricalRecord>(mHistoricalRecords), mHistoryFileName);
610 }
611 }
613 /**
614 * Sets the sorter for ordering activities based on historical data and an intent.
615 *
616 * @param activitySorter The sorter.
617 *
618 * @see ActivitySorter
619 */
620 public void setActivitySorter(ActivitySorter activitySorter) {
621 synchronized (mInstanceLock) {
622 if (mActivitySorter == activitySorter) {
623 return;
624 }
625 mActivitySorter = activitySorter;
626 if (sortActivitiesIfNeeded()) {
627 notifyChanged();
628 }
629 }
630 }
632 /**
633 * Sets the maximal size of the historical data. Defaults to
634 * {@link #DEFAULT_HISTORY_MAX_LENGTH}
635 * <p>
636 * <strong>Note:</strong> Setting this property will immediately
637 * enforce the specified max history size by dropping enough old
638 * historical records to enforce the desired size. Thus, any
639 * records that exceed the history size will be discarded and
640 * irreversibly lost.
641 * </p>
642 *
643 * @param historyMaxSize The max history size.
644 */
645 public void setHistoryMaxSize(int historyMaxSize) {
646 synchronized (mInstanceLock) {
647 if (mHistoryMaxSize == historyMaxSize) {
648 return;
649 }
650 mHistoryMaxSize = historyMaxSize;
651 pruneExcessiveHistoricalRecordsIfNeeded();
652 if (sortActivitiesIfNeeded()) {
653 notifyChanged();
654 }
655 }
656 }
658 /**
659 * Gets the history max size.
660 *
661 * @return The history max size.
662 */
663 public int getHistoryMaxSize() {
664 synchronized (mInstanceLock) {
665 return mHistoryMaxSize;
666 }
667 }
669 /**
670 * Gets the history size.
671 *
672 * @return The history size.
673 */
674 public int getHistorySize() {
675 synchronized (mInstanceLock) {
676 ensureConsistentState();
677 return mHistoricalRecords.size();
678 }
679 }
681 public int getDistinctActivityCountInHistory() {
682 synchronized (mInstanceLock) {
683 ensureConsistentState();
684 final List<String> packages = new ArrayList<String>();
685 for (HistoricalRecord record : mHistoricalRecords) {
686 String activity = record.activity.flattenToString();
687 if (!packages.contains(activity)) {
688 packages.add(activity);
689 }
690 }
691 return packages.size();
692 }
693 }
695 @Override
696 protected void finalize() throws Throwable {
697 super.finalize();
699 /**
700 * Mozilla: Not needed for the application.
701 */
702 mPackageMonitor.unregister();
703 }
705 /**
706 * Ensures the model is in a consistent state which is the
707 * activities for the current intent have been loaded, the
708 * most recent history has been read, and the activities
709 * are sorted.
710 */
711 private void ensureConsistentState() {
712 boolean stateChanged = loadActivitiesIfNeeded();
713 stateChanged |= readHistoricalDataIfNeeded();
714 pruneExcessiveHistoricalRecordsIfNeeded();
715 if (stateChanged) {
716 sortActivitiesIfNeeded();
717 notifyChanged();
718 }
719 }
721 /**
722 * Sorts the activities if necessary which is if there is a
723 * sorter, there are some activities to sort, and there is some
724 * historical data.
725 *
726 * @return Whether sorting was performed.
727 */
728 private boolean sortActivitiesIfNeeded() {
729 if (mActivitySorter != null && mIntent != null
730 && !mActivities.isEmpty() && !mHistoricalRecords.isEmpty()) {
731 mActivitySorter.sort(mIntent, mActivities,
732 Collections.unmodifiableList(mHistoricalRecords));
733 return true;
734 }
735 return false;
736 }
738 /**
739 * Loads the activities for the current intent if needed which is
740 * if they are not already loaded for the current intent.
741 *
742 * @return Whether loading was performed.
743 */
744 private boolean loadActivitiesIfNeeded() {
745 if (mReloadActivities && mIntent != null) {
746 mReloadActivities = false;
747 mActivities.clear();
748 List<ResolveInfo> resolveInfos = mContext.getPackageManager()
749 .queryIntentActivities(mIntent, 0);
750 final int resolveInfoCount = resolveInfos.size();
751 for (int i = 0; i < resolveInfoCount; i++) {
752 ResolveInfo resolveInfo = resolveInfos.get(i);
753 mActivities.add(new ActivityResolveInfo(resolveInfo));
754 }
755 return true;
756 }
757 return false;
758 }
760 /**
761 * Reads the historical data if necessary which is it has
762 * changed, there is a history file, and there is not persist
763 * in progress.
764 *
765 * @return Whether reading was performed.
766 */
767 private boolean readHistoricalDataIfNeeded() {
768 if (mCanReadHistoricalData && mHistoricalRecordsChanged &&
769 !TextUtils.isEmpty(mHistoryFileName)) {
770 mCanReadHistoricalData = false;
771 mReadShareHistoryCalled = true;
772 readHistoricalDataImpl();
773 return true;
774 }
775 return false;
776 }
778 /**
779 * Adds a historical record.
780 *
781 * @param historicalRecord The record to add.
782 * @return True if the record was added.
783 */
784 private boolean addHistoricalRecord(HistoricalRecord historicalRecord) {
785 final boolean added = mHistoricalRecords.add(historicalRecord);
786 if (added) {
787 mHistoricalRecordsChanged = true;
788 pruneExcessiveHistoricalRecordsIfNeeded();
789 persistHistoricalDataIfNeeded();
790 sortActivitiesIfNeeded();
791 notifyChanged();
792 }
793 return added;
794 }
796 /**
797 * Removes all historical records for this pkg.
798 *
799 * @param historicalRecord The pkg to delete records for.
800 * @return True if the record was added.
801 */
802 private boolean removeHistoricalRecordsForPackage(final String pkg) {
803 boolean removed = false;
805 for (Iterator<HistoricalRecord> i = mHistoricalRecords.iterator(); i.hasNext();) {
806 final HistoricalRecord record = i.next();
807 if (record.activity.getPackageName().equals(pkg)) {
808 i.remove();
809 removed = true;
810 }
811 }
813 if (removed) {
814 mHistoricalRecordsChanged = true;
815 pruneExcessiveHistoricalRecordsIfNeeded();
816 persistHistoricalDataIfNeeded();
817 sortActivitiesIfNeeded();
818 notifyChanged();
819 }
821 return removed;
822 }
824 /**
825 * Prunes older excessive records to guarantee maxHistorySize.
826 */
827 private void pruneExcessiveHistoricalRecordsIfNeeded() {
828 final int pruneCount = mHistoricalRecords.size() - mHistoryMaxSize;
829 if (pruneCount <= 0) {
830 return;
831 }
832 mHistoricalRecordsChanged = true;
833 for (int i = 0; i < pruneCount; i++) {
834 HistoricalRecord prunedRecord = mHistoricalRecords.remove(0);
835 if (DEBUG) {
836 Log.i(LOG_TAG, "Pruned: " + prunedRecord);
837 }
838 }
839 }
841 /**
842 * Represents a record in the history.
843 */
844 public final static class HistoricalRecord {
846 /**
847 * The activity name.
848 */
849 public final ComponentName activity;
851 /**
852 * The choice time.
853 */
854 public final long time;
856 /**
857 * The record weight.
858 */
859 public final float weight;
861 /**
862 * Creates a new instance.
863 *
864 * @param activityName The activity component name flattened to string.
865 * @param time The time the activity was chosen.
866 * @param weight The weight of the record.
867 */
868 public HistoricalRecord(String activityName, long time, float weight) {
869 this(ComponentName.unflattenFromString(activityName), time, weight);
870 }
872 /**
873 * Creates a new instance.
874 *
875 * @param activityName The activity name.
876 * @param time The time the activity was chosen.
877 * @param weight The weight of the record.
878 */
879 public HistoricalRecord(ComponentName activityName, long time, float weight) {
880 this.activity = activityName;
881 this.time = time;
882 this.weight = weight;
883 }
885 @Override
886 public int hashCode() {
887 final int prime = 31;
888 int result = 1;
889 result = prime * result + ((activity == null) ? 0 : activity.hashCode());
890 result = prime * result + (int) (time ^ (time >>> 32));
891 result = prime * result + Float.floatToIntBits(weight);
892 return result;
893 }
895 @Override
896 public boolean equals(Object obj) {
897 if (this == obj) {
898 return true;
899 }
900 if (obj == null) {
901 return false;
902 }
903 if (getClass() != obj.getClass()) {
904 return false;
905 }
906 HistoricalRecord other = (HistoricalRecord) obj;
907 if (activity == null) {
908 if (other.activity != null) {
909 return false;
910 }
911 } else if (!activity.equals(other.activity)) {
912 return false;
913 }
914 if (time != other.time) {
915 return false;
916 }
917 if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) {
918 return false;
919 }
920 return true;
921 }
923 @Override
924 public String toString() {
925 StringBuilder builder = new StringBuilder();
926 builder.append("[");
927 builder.append("; activity:").append(activity);
928 builder.append("; time:").append(time);
929 builder.append("; weight:").append(new BigDecimal(weight));
930 builder.append("]");
931 return builder.toString();
932 }
933 }
935 /**
936 * Represents an activity.
937 */
938 public final class ActivityResolveInfo implements Comparable<ActivityResolveInfo> {
940 /**
941 * The {@link ResolveInfo} of the activity.
942 */
943 public final ResolveInfo resolveInfo;
945 /**
946 * Weight of the activity. Useful for sorting.
947 */
948 public float weight;
950 /**
951 * Creates a new instance.
952 *
953 * @param resolveInfo activity {@link ResolveInfo}.
954 */
955 public ActivityResolveInfo(ResolveInfo resolveInfo) {
956 this.resolveInfo = resolveInfo;
957 }
959 @Override
960 public int hashCode() {
961 return 31 + Float.floatToIntBits(weight);
962 }
964 @Override
965 public boolean equals(Object obj) {
966 if (this == obj) {
967 return true;
968 }
969 if (obj == null) {
970 return false;
971 }
972 if (getClass() != obj.getClass()) {
973 return false;
974 }
975 ActivityResolveInfo other = (ActivityResolveInfo) obj;
976 if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) {
977 return false;
978 }
979 return true;
980 }
982 public int compareTo(ActivityResolveInfo another) {
983 return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight);
984 }
986 @Override
987 public String toString() {
988 StringBuilder builder = new StringBuilder();
989 builder.append("[");
990 builder.append("resolveInfo:").append(resolveInfo.toString());
991 builder.append("; weight:").append(new BigDecimal(weight));
992 builder.append("]");
993 return builder.toString();
994 }
995 }
997 /**
998 * Default activity sorter implementation.
999 */
1000 private final class DefaultSorter implements ActivitySorter {
1001 private static final float WEIGHT_DECAY_COEFFICIENT = 0.95f;
1003 private final Map<String, ActivityResolveInfo> mPackageNameToActivityMap =
1004 new HashMap<String, ActivityResolveInfo>();
1006 public void sort(Intent intent, List<ActivityResolveInfo> activities,
1007 List<HistoricalRecord> historicalRecords) {
1008 Map<String, ActivityResolveInfo> packageNameToActivityMap =
1009 mPackageNameToActivityMap;
1010 packageNameToActivityMap.clear();
1012 final int activityCount = activities.size();
1013 for (int i = 0; i < activityCount; i++) {
1014 ActivityResolveInfo activity = activities.get(i);
1015 activity.weight = 0.0f;
1017 // Make sure we're using a non-ambiguous name here
1018 ComponentName chosenName = new ComponentName(
1019 activity.resolveInfo.activityInfo.packageName,
1020 activity.resolveInfo.activityInfo.name);
1021 String packageName = chosenName.flattenToString();
1022 packageNameToActivityMap.put(packageName, activity);
1023 }
1025 final int lastShareIndex = historicalRecords.size() - 1;
1026 float nextRecordWeight = 1;
1027 for (int i = lastShareIndex; i >= 0; i--) {
1028 HistoricalRecord historicalRecord = historicalRecords.get(i);
1029 String packageName = historicalRecord.activity.flattenToString();
1030 ActivityResolveInfo activity = packageNameToActivityMap.get(packageName);
1031 if (activity != null) {
1032 activity.weight += historicalRecord.weight * nextRecordWeight;
1033 nextRecordWeight = nextRecordWeight * WEIGHT_DECAY_COEFFICIENT;
1034 }
1035 }
1037 Collections.sort(activities);
1039 if (DEBUG) {
1040 for (int i = 0; i < activityCount; i++) {
1041 Log.i(LOG_TAG, "Sorted: " + activities.get(i));
1042 }
1043 }
1044 }
1045 }
1047 /**
1048 * Command for reading the historical records from a file off the UI thread.
1049 */
1050 private void readHistoricalDataImpl() {
1051 FileInputStream fis = null;
1052 try {
1053 GeckoProfile profile = GeckoProfile.get(mContext);
1054 File f = profile.getFile(mHistoryFileName);
1055 if (!f.exists()) {
1056 // Fall back to the non-profile aware file if it exists...
1057 File oldFile = new File(mHistoryFileName);
1058 oldFile.renameTo(f);
1059 }
1060 fis = new FileInputStream(f);
1061 } catch (FileNotFoundException fnfe) {
1062 try {
1063 Distribution dist = new Distribution(mContext);
1064 File distFile = dist.getDistributionFile("quickshare/" + mHistoryFileName);
1065 if (distFile == null) {
1066 if (DEBUG) {
1067 Log.i(LOG_TAG, "Could not open historical records file: " + mHistoryFileName);
1068 }
1069 return;
1070 }
1071 fis = new FileInputStream(distFile);
1072 } catch(Exception ex) {
1073 if (DEBUG) {
1074 Log.i(LOG_TAG, "Could not open historical records file: " + mHistoryFileName);
1075 }
1076 return;
1077 }
1078 }
1080 try {
1081 XmlPullParser parser = Xml.newPullParser();
1082 parser.setInput(fis, null);
1084 int type = XmlPullParser.START_DOCUMENT;
1085 while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) {
1086 type = parser.next();
1087 }
1089 if (!TAG_HISTORICAL_RECORDS.equals(parser.getName())) {
1090 throw new XmlPullParserException("Share records file does not start with "
1091 + TAG_HISTORICAL_RECORDS + " tag.");
1092 }
1094 List<HistoricalRecord> historicalRecords = mHistoricalRecords;
1095 historicalRecords.clear();
1097 while (true) {
1098 type = parser.next();
1099 if (type == XmlPullParser.END_DOCUMENT) {
1100 break;
1101 }
1102 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
1103 continue;
1104 }
1105 String nodeName = parser.getName();
1106 if (!TAG_HISTORICAL_RECORD.equals(nodeName)) {
1107 throw new XmlPullParserException("Share records file not well-formed.");
1108 }
1110 String activity = parser.getAttributeValue(null, ATTRIBUTE_ACTIVITY);
1111 final long time =
1112 Long.parseLong(parser.getAttributeValue(null, ATTRIBUTE_TIME));
1113 final float weight =
1114 Float.parseFloat(parser.getAttributeValue(null, ATTRIBUTE_WEIGHT));
1115 HistoricalRecord readRecord = new HistoricalRecord(activity, time, weight);
1116 historicalRecords.add(readRecord);
1118 if (DEBUG) {
1119 Log.i(LOG_TAG, "Read " + readRecord.toString());
1120 }
1121 }
1123 if (DEBUG) {
1124 Log.i(LOG_TAG, "Read " + historicalRecords.size() + " historical records.");
1125 }
1126 } catch (XmlPullParserException xppe) {
1127 Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, xppe);
1128 } catch (IOException ioe) {
1129 Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, ioe);
1130 } finally {
1131 if (fis != null) {
1132 try {
1133 fis.close();
1134 } catch (IOException ioe) {
1135 /* ignore */
1136 }
1137 }
1138 }
1139 }
1141 /**
1142 * Command for persisting the historical records to a file off the UI thread.
1143 */
1144 private final class PersistHistoryAsyncTask extends AsyncTask<Object, Void, Void> {
1146 @Override
1147 @SuppressWarnings("unchecked")
1148 public Void doInBackground(Object... args) {
1149 List<HistoricalRecord> historicalRecords = (List<HistoricalRecord>) args[0];
1150 String historyFileName = (String) args[1];
1152 FileOutputStream fos = null;
1154 try {
1155 // Mozilla - Update the location we save files to
1156 GeckoProfile profile = GeckoProfile.get(mContext);
1157 File file = profile.getFile(historyFileName);
1158 fos = new FileOutputStream(file);
1159 } catch (FileNotFoundException fnfe) {
1160 Log.e(LOG_TAG, "Error writing historical record file: " + historyFileName, fnfe);
1161 return null;
1162 }
1164 XmlSerializer serializer = Xml.newSerializer();
1166 try {
1167 serializer.setOutput(fos, null);
1168 serializer.startDocument("UTF-8", true);
1169 serializer.startTag(null, TAG_HISTORICAL_RECORDS);
1171 final int recordCount = historicalRecords.size();
1172 for (int i = 0; i < recordCount; i++) {
1173 HistoricalRecord record = historicalRecords.remove(0);
1174 serializer.startTag(null, TAG_HISTORICAL_RECORD);
1175 serializer.attribute(null, ATTRIBUTE_ACTIVITY,
1176 record.activity.flattenToString());
1177 serializer.attribute(null, ATTRIBUTE_TIME, String.valueOf(record.time));
1178 serializer.attribute(null, ATTRIBUTE_WEIGHT, String.valueOf(record.weight));
1179 serializer.endTag(null, TAG_HISTORICAL_RECORD);
1180 if (DEBUG) {
1181 Log.i(LOG_TAG, "Wrote " + record.toString());
1182 }
1183 }
1185 serializer.endTag(null, TAG_HISTORICAL_RECORDS);
1186 serializer.endDocument();
1188 if (DEBUG) {
1189 Log.i(LOG_TAG, "Wrote " + recordCount + " historical records.");
1190 }
1191 } catch (IllegalArgumentException iae) {
1192 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, iae);
1193 } catch (IllegalStateException ise) {
1194 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ise);
1195 } catch (IOException ioe) {
1196 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ioe);
1197 } finally {
1198 mCanReadHistoricalData = true;
1199 if (fos != null) {
1200 try {
1201 fos.close();
1202 } catch (IOException e) {
1203 /* ignore */
1204 }
1205 }
1206 }
1207 return null;
1208 }
1209 }
1211 /**
1212 * Keeps in sync the historical records and activities with the installed applications.
1213 */
1214 /**
1215 * Mozilla: Adapted significantly
1216 */
1217 private static final String LOGTAG = "GeckoActivityChooserModel";
1218 private final class DataModelPackageMonitor extends BroadcastReceiver {
1219 private Context mContext;
1221 public DataModelPackageMonitor() { }
1223 public void register(Context context) {
1224 mContext = context;
1226 String[] intents = new String[] {
1227 Intent.ACTION_PACKAGE_REMOVED,
1228 Intent.ACTION_PACKAGE_ADDED,
1229 Intent.ACTION_PACKAGE_CHANGED
1230 };
1232 for (String intent : intents) {
1233 IntentFilter removeFilter = new IntentFilter(intent);
1234 removeFilter.addDataScheme("package");
1235 context.registerReceiver(this, removeFilter);
1236 }
1237 }
1239 public void unregister() {
1240 mContext.unregisterReceiver(this);
1241 mContext = null;
1242 }
1244 @Override
1245 public void onReceive(Context context, Intent intent) {
1246 String action = intent.getAction();
1247 if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
1248 String packageName = intent.getData().getSchemeSpecificPart();
1249 removeHistoricalRecordsForPackage(packageName);
1250 }
1252 mReloadActivities = true;
1253 }
1254 }
1255 }