2 * Copyright (C) 2011 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com
.actionbarsherlock
.widget
;
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
;
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
;
44 import java
.util
.concurrent
.Executor
;
45 import java
.util
.concurrent
.Executors
;
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.
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.
69 * The way clients interact with this class is as follows:
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");
78 * ActivityChooserModelClient modelClient1 = getActivityChooserModelClient1();
79 * modelClient1.setActivityChooserModel(dataModel);
81 * ActivityChooserModelClient modelClient2 = getActivityChooserModelClient2();
82 * modelClient2.setActivityChooserModel(dataModel);
84 * // Set an intent to choose a an activity for.
85 * dataModel.setIntent(intent);
90 * <strong>Note:</strong> This class is thread safe.
95 class ActivityChooserModel
extends DataSetObservable
{
98 * Client that utilizes an {@link ActivityChooserModel}.
100 public interface ActivityChooserModelClient
{
103 * Sets the {@link ActivityChooserModel}.
105 * @param dataModel The model.
107 public void setActivityChooserModel(ActivityChooserModel dataModel
);
111 * Defines a sorter that is responsible for sorting the activities
112 * based on the provided historical choices and an intent.
114 public interface ActivitySorter
{
117 * Sorts the <code>activities</code> in descending order of relevance
118 * based on previous history and an intent.
120 * @param intent The {@link Intent}.
121 * @param activities Activities to be sorted.
122 * @param historicalRecords Historical records.
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
);
131 * Listener for choosing an activity.
133 public interface OnChooseActivityListener
{
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}
141 * <strong>Note:</strong> Modifying the intent is not permitted and
142 * any changes to the latter will be ignored.
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.
149 * @see ActivityChooserModel#chooseActivity(int)
151 public boolean onChooseActivity(ActivityChooserModel host
, Intent intent
);
155 * Flag for selecting debug mode.
157 private static final boolean DEBUG
= false
;
160 * Tag used for logging.
162 private static final String LOG_TAG
= ActivityChooserModel
.class.getSimpleName();
165 * The root tag in the history file.
167 private static final String TAG_HISTORICAL_RECORDS
= "historical-records";
170 * The tag for a record in the history file.
172 private static final String TAG_HISTORICAL_RECORD
= "historical-record";
175 * Attribute for the activity.
177 private static final String ATTRIBUTE_ACTIVITY
= "activity";
180 * Attribute for the choice time.
182 private static final String ATTRIBUTE_TIME
= "time";
185 * Attribute for the choice weight.
187 private static final String ATTRIBUTE_WEIGHT
= "weight";
190 * The default name of the choice history file.
192 public static final String DEFAULT_HISTORY_FILE_NAME
=
193 "activity_choser_model_history.xml";
196 * The default maximal length of the choice history.
198 public static final int DEFAULT_HISTORY_MAX_LENGTH
= 50;
201 * The amount with which to inflate a chosen activity when set as default.
203 private static final int DEFAULT_ACTIVITY_INFLATION
= 5;
206 * Default weight for a choice record.
208 private static final float DEFAULT_HISTORICAL_RECORD_WEIGHT
= 1.0f
;
211 * The extension of the history file.
213 private static final String HISTORY_FILE_EXTENSION
= ".xml";
216 * An invalid item index.
218 private static final int INVALID_INDEX
= -1;
221 * Lock to guard the model registry.
223 private static final Object sRegistryLock
= new Object();
226 * This the registry for data models.
228 private static final Map
<String
, ActivityChooserModel
> sDataModelRegistry
=
229 new HashMap
<String
, ActivityChooserModel
>();
232 * Lock for synchronizing on this instance.
234 private final Object mInstanceLock
= new Object();
237 * List of activities that can handle the current intent.
239 private final List
<ActivityResolveInfo
> mActivites
= new ArrayList
<ActivityResolveInfo
>();
242 * List with historical choice records.
244 private final List
<HistoricalRecord
> mHistoricalRecords
= new ArrayList
<HistoricalRecord
>();
247 * Context for accessing resources.
249 private final Context mContext
;
252 * The name of the history file that backs this model.
254 private final String mHistoryFileName
;
257 * The intent for which a activity is being chosen.
259 private Intent mIntent
;
262 * The sorter for ordering activities based on intent and past choices.
264 private ActivitySorter mActivitySorter
= new DefaultSorter();
267 * The maximal length of the choice history.
269 private int mHistoryMaxSize
= DEFAULT_HISTORY_MAX_LENGTH
;
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.
279 private boolean mCanReadHistoricalData
= true
;
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.
290 private boolean mReadShareHistoryCalled
= false
;
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.
298 private boolean mHistoricalRecordsChanged
= true
;
301 * Hander for scheduling work on client tread.
303 private final Handler mHandler
= new Handler();
306 * Policy for controlling how the model handles chosen activities.
308 private OnChooseActivityListener mActivityChoserModelPolicy
;
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
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.
323 * <strong>Always use difference historical data files for semantically different actions.
324 * For example, sharing is different from importing.</strong>
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.
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
);
341 dataModel
.readHistoricalData();
347 * Creates a new instance.
349 * @param context Context for loading resources.
350 * @param historyFileName The history XML file.
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
;
358 mHistoryFileName
= historyFileName
;
363 * Sets an intent for which to choose a activity.
365 * <strong>Note:</strong> Clients must set only semantically similar
366 * intents for each data model.
369 * @param intent The intent.
371 public void setIntent(Intent intent
) {
372 synchronized (mInstanceLock
) {
373 if (mIntent
== intent
) {
377 loadActivitiesLocked();
382 * Gets the intent for which a activity is being chosen.
384 * @return The intent.
386 public Intent
getIntent() {
387 synchronized (mInstanceLock
) {
393 * Gets the number of activities that can handle the intent.
395 * @return The activity count.
397 * @see #setIntent(Intent)
399 public int getActivityCount() {
400 synchronized (mInstanceLock
) {
401 return mActivites
.size();
406 * Gets an activity at a given index.
408 * @return The activity.
410 * @see ActivityResolveInfo
411 * @see #setIntent(Intent)
413 public ResolveInfo
getActivity(int index
) {
414 synchronized (mInstanceLock
) {
415 return mActivites
.get(index
).resolveInfo
;
420 * Gets the index of a the given activity.
422 * @param activity The activity index.
424 * @return The index if found, -1 otherwise.
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
) {
435 return INVALID_INDEX
;
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
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.
449 * @return An {@link Intent} for launching the activity or null if the
450 * policy has consumed the intent.
452 * @see HistoricalRecord
453 * @see OnChooseActivityListener
455 public Intent
chooseActivity(int index
) {
456 ActivityResolveInfo chosenActivity
= mActivites
.get(index
);
458 ComponentName chosenName
= new ComponentName(
459 chosenActivity
.resolveInfo
.activityInfo
.packageName
,
460 chosenActivity
.resolveInfo
.activityInfo
.name
);
462 Intent choiceIntent
= new Intent(mIntent
);
463 choiceIntent
.setComponent(chosenName
);
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,
475 HistoricalRecord historicalRecord
= new HistoricalRecord(chosenName
,
476 System
.currentTimeMillis(), DEFAULT_HISTORICAL_RECORD_WEIGHT
);
477 addHisoricalRecord(historicalRecord
);
483 * Sets the listener for choosing an activity.
485 * @param listener The listener.
487 public void setOnChooseActivityListener(OnChooseActivityListener listener
) {
488 mActivityChoserModelPolicy
= listener
;
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
496 * @return The default activity, <code>null</code> id not activities.
498 * @see #getActivity(int)
500 public ResolveInfo
getDefaultActivity() {
501 synchronized (mInstanceLock
) {
502 if (!mActivites
.isEmpty()) {
503 return mActivites
.get(0).resolveInfo
;
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.
517 * @param index The index of the activity to set as default.
519 public void setDefaultActivity(int index
) {
520 ActivityResolveInfo newDefaultActivity
= mActivites
.get(index
);
521 ActivityResolveInfo oldDefaultActivity
= mActivites
.get(0);
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
;
529 weight
= DEFAULT_HISTORICAL_RECORD_WEIGHT
;
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
);
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.
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.
551 private void readHistoricalData() {
552 synchronized (mInstanceLock
) {
553 if (!mCanReadHistoricalData
|| !mHistoricalRecordsChanged
) {
556 mCanReadHistoricalData
= false
;
557 mReadShareHistoryCalled
= true
;
558 if (!TextUtils
.isEmpty(mHistoryFileName
)) {
559 /*AsyncTask.*/SERIAL_EXECUTOR
.execute(new HistoryLoader());
564 private static final Executor SERIAL_EXECUTOR
= Executors
.newSingleThreadExecutor();
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.
572 * @throws IllegalStateException If this method is called before a call to
573 * {@link #readHistoricalData()}.
575 private void persistHistoricalData() {
576 synchronized (mInstanceLock
) {
577 if (!mReadShareHistoryCalled
) {
578 throw new IllegalStateException("No preceding call to #readHistoricalData");
580 if (!mHistoricalRecordsChanged
) {
583 mHistoricalRecordsChanged
= false
;
584 mCanReadHistoricalData
= true
;
585 if (!TextUtils
.isEmpty(mHistoryFileName
)) {
586 /*AsyncTask.*/SERIAL_EXECUTOR
.execute(new HistoryPersister());
592 * Sets the sorter for ordering activities based on historical data and an intent.
594 * @param activitySorter The sorter.
596 * @see ActivitySorter
598 public void setActivitySorter(ActivitySorter activitySorter
) {
599 synchronized (mInstanceLock
) {
600 if (mActivitySorter
== activitySorter
) {
603 mActivitySorter
= activitySorter
;
609 * Sorts the activities based on history and an intent. If
610 * a sorter is not specified this a default implementation is used.
612 * @see #setActivitySorter(ActivitySorter)
614 private void sortActivities() {
615 synchronized (mInstanceLock
) {
616 if (mActivitySorter
!= null
&& !mActivites
.isEmpty()) {
617 mActivitySorter
.sort(mIntent
, mActivites
,
618 Collections
.unmodifiableList(mHistoricalRecords
));
625 * Sets the maximal size of the historical data. Defaults to
626 * {@link #DEFAULT_HISTORY_MAX_LENGTH}
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
635 * @param historyMaxSize The max history size.
637 public void setHistoryMaxSize(int historyMaxSize
) {
638 synchronized (mInstanceLock
) {
639 if (mHistoryMaxSize
== historyMaxSize
) {
642 mHistoryMaxSize
= historyMaxSize
;
643 pruneExcessiveHistoricalRecordsLocked();
649 * Gets the history max size.
651 * @return The history max size.
653 public int getHistoryMaxSize() {
654 synchronized (mInstanceLock
) {
655 return mHistoryMaxSize
;
660 * Gets the history size.
662 * @return The history size.
664 public int getHistorySize() {
665 synchronized (mInstanceLock
) {
666 return mHistoricalRecords
.size();
671 * Adds a historical record.
673 * @param historicalRecord The record to add.
674 * @return True if the record was added.
676 private boolean addHisoricalRecord(HistoricalRecord historicalRecord
) {
677 synchronized (mInstanceLock
) {
678 final boolean added
= mHistoricalRecords
.add(historicalRecord
);
680 mHistoricalRecordsChanged
= true
;
681 pruneExcessiveHistoricalRecordsLocked();
682 persistHistoricalData();
690 * Prunes older excessive records to guarantee {@link #mHistoryMaxSize}.
692 private void pruneExcessiveHistoricalRecordsLocked() {
693 List
<HistoricalRecord
> choiceRecords
= mHistoricalRecords
;
694 final int pruneCount
= choiceRecords
.size() - mHistoryMaxSize
;
695 if (pruneCount
<= 0) {
698 mHistoricalRecordsChanged
= true
;
699 for (int i
= 0; i
< pruneCount
; i
++) {
700 HistoricalRecord prunedRecord
= choiceRecords
.remove(0);
702 Log
.i(LOG_TAG
, "Pruned: " + prunedRecord
);
708 * Loads the activities.
710 private void loadActivitiesLocked() {
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
));
727 * Represents a record in the history.
729 public final static class HistoricalRecord
{
734 public final ComponentName activity
;
739 public final long time
;
744 public final float weight
;
747 * Creates a new instance.
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.
753 public HistoricalRecord(String activityName
, long time
, float weight
) {
754 this(ComponentName
.unflattenFromString(activityName
), time
, weight
);
758 * Creates a new instance.
760 * @param activityName The activity name.
761 * @param time The time the activity was chosen.
762 * @param weight The weight of the record.
764 public HistoricalRecord(ComponentName activityName
, long time
, float weight
) {
765 this.activity
= activityName
;
767 this.weight
= weight
;
771 public int hashCode() {
772 final int prime
= 31;
774 result
= prime
* result
+ ((activity
== null
) ?
0 : activity
.hashCode());
775 result
= prime
* result
+ (int) (time ^
(time
>>> 32));
776 result
= prime
* result
+ Float
.floatToIntBits(weight
);
781 public boolean equals(Object obj
) {
788 if (getClass() != obj
.getClass()) {
791 HistoricalRecord other
= (HistoricalRecord
) obj
;
792 if (activity
== null
) {
793 if (other
.activity
!= null
) {
796 } else if (!activity
.equals(other
.activity
)) {
799 if (time
!= other
.time
) {
802 if (Float
.floatToIntBits(weight
) != Float
.floatToIntBits(other
.weight
)) {
809 public String
toString() {
810 StringBuilder builder
= new StringBuilder();
812 builder
.append("; activity:").append(activity
);
813 builder
.append("; time:").append(time
);
814 builder
.append("; weight:").append(new BigDecimal(weight
));
816 return builder
.toString();
821 * Represents an activity.
823 public final class ActivityResolveInfo
implements Comparable
<ActivityResolveInfo
> {
826 * The {@link ResolveInfo} of the activity.
828 public final ResolveInfo resolveInfo
;
831 * Weight of the activity. Useful for sorting.
836 * Creates a new instance.
838 * @param resolveInfo activity {@link ResolveInfo}.
840 public ActivityResolveInfo(ResolveInfo resolveInfo
) {
841 this.resolveInfo
= resolveInfo
;
845 public int hashCode() {
846 return 31 + Float
.floatToIntBits(weight
);
850 public boolean equals(Object obj
) {
857 if (getClass() != obj
.getClass()) {
860 ActivityResolveInfo other
= (ActivityResolveInfo
) obj
;
861 if (Float
.floatToIntBits(weight
) != Float
.floatToIntBits(other
.weight
)) {
867 public int compareTo(ActivityResolveInfo another
) {
868 return Float
.floatToIntBits(another
.weight
) - Float
.floatToIntBits(weight
);
872 public String
toString() {
873 StringBuilder builder
= new StringBuilder();
875 builder
.append("resolveInfo:").append(resolveInfo
.toString());
876 builder
.append("; weight:").append(new BigDecimal(weight
));
878 return builder
.toString();
883 * Default activity sorter implementation.
885 private final class DefaultSorter
implements ActivitySorter
{
886 private static final float WEIGHT_DECAY_COEFFICIENT
= 0.95f
;
888 private final Map
<String
, ActivityResolveInfo
> mPackageNameToActivityMap
=
889 new HashMap
<String
, ActivityResolveInfo
>();
891 public void sort(Intent intent
, List
<ActivityResolveInfo
> activities
,
892 List
<HistoricalRecord
> historicalRecords
) {
893 Map
<String
, ActivityResolveInfo
> packageNameToActivityMap
=
894 mPackageNameToActivityMap
;
895 packageNameToActivityMap
.clear();
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
);
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
;
917 Collections
.sort(activities
);
920 for (int i
= 0; i
< activityCount
; i
++) {
921 Log
.i(LOG_TAG
, "Sorted: " + activities
.get(i
));
928 * Command for reading the historical records from a file off the UI thread.
930 private final class HistoryLoader
implements Runnable
{
933 FileInputStream fis
= null
;
935 fis
= mContext
.openFileInput(mHistoryFileName
);
936 } catch (FileNotFoundException fnfe
) {
938 Log
.i(LOG_TAG
, "Could not open historical records file: " + mHistoryFileName
);
943 XmlPullParser parser
= Xml
.newPullParser();
944 parser
.setInput(fis
, null
);
946 int type
= XmlPullParser
.START_DOCUMENT
;
947 while (type
!= XmlPullParser
.END_DOCUMENT
&& type
!= XmlPullParser
.START_TAG
) {
948 type
= parser
.next();
951 if (!TAG_HISTORICAL_RECORDS
.equals(parser
.getName())) {
952 throw new XmlPullParserException("Share records file does not start with "
953 + TAG_HISTORICAL_RECORDS
+ " tag.");
956 List
<HistoricalRecord
> readRecords
= new ArrayList
<HistoricalRecord
>();
959 type
= parser
.next();
960 if (type
== XmlPullParser
.END_DOCUMENT
) {
963 if (type
== XmlPullParser
.END_TAG
|| type
== XmlPullParser
.TEXT
) {
966 String nodeName
= parser
.getName();
967 if (!TAG_HISTORICAL_RECORD
.equals(nodeName
)) {
968 throw new XmlPullParserException("Share records file not well-formed.");
971 String activity
= parser
.getAttributeValue(null
, ATTRIBUTE_ACTIVITY
);
973 Long
.parseLong(parser
.getAttributeValue(null
, ATTRIBUTE_TIME
));
975 Float
.parseFloat(parser
.getAttributeValue(null
, ATTRIBUTE_WEIGHT
));
977 HistoricalRecord readRecord
= new HistoricalRecord(activity
, time
,
979 readRecords
.add(readRecord
);
982 Log
.i(LOG_TAG
, "Read " + readRecord
.toString());
987 Log
.i(LOG_TAG
, "Read " + readRecords
.size() + " historical records.");
990 synchronized (mInstanceLock
) {
991 Set
<HistoricalRecord
> uniqueShareRecords
=
992 new LinkedHashSet
<HistoricalRecord
>(readRecords
);
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
);
1005 if (historicalRecords
.size() == uniqueShareRecords
.size()) {
1009 // Make sure the oldest records go to the end.
1010 historicalRecords
.clear();
1011 historicalRecords
.addAll(uniqueShareRecords
);
1013 mHistoricalRecordsChanged
= true
;
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() {
1020 pruneExcessiveHistoricalRecordsLocked();
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
);
1033 } catch (IOException ioe
) {
1042 * Command for persisting the historical records to a file off the UI thread.
1044 private final class HistoryPersister
implements Runnable
{
1047 FileOutputStream fos
= null
;
1048 List
<HistoricalRecord
> records
= null
;
1050 synchronized (mInstanceLock
) {
1051 records
= new ArrayList
<HistoricalRecord
>(mHistoricalRecords
);
1055 fos
= mContext
.openFileOutput(mHistoryFileName
, Context
.MODE_PRIVATE
);
1056 } catch (FileNotFoundException fnfe
) {
1057 Log
.e(LOG_TAG
, "Error writing historical recrod file: " + mHistoryFileName
, fnfe
);
1061 XmlSerializer serializer
= Xml
.newSerializer();
1064 serializer
.setOutput(fos
, null
);
1065 serializer
.startDocument("UTF-8", true
);
1066 serializer
.startTag(null
, TAG_HISTORICAL_RECORDS
);
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
);
1077 Log
.i(LOG_TAG
, "Wrote " + record
.toString());
1081 serializer
.endTag(null
, TAG_HISTORICAL_RECORDS
);
1082 serializer
.endDocument();
1085 Log
.i(LOG_TAG
, "Wrote " + recordCount
+ " historical records.");
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
);
1097 } catch (IOException e
) {