blob: 51174c315c77f610f282476088f03347d57b1d1f [file] [log] [blame]
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
Svetoslavbaeabb62013-10-28 15:22:14 -070019import android.app.ActivityManager;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070020import android.content.ComponentName;
21import android.content.Context;
22import android.content.Intent;
Svetoslavbaeabb62013-10-28 15:22:14 -070023import android.content.pm.ActivityInfo;
24import android.content.pm.PackageManager;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070025import android.content.pm.ResolveInfo;
26import android.database.DataSetObservable;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070027import android.os.AsyncTask;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070028import android.text.TextUtils;
29import android.util.Log;
30import android.util.Xml;
31
32import com.android.internal.content.PackageMonitor;
33
34import org.xmlpull.v1.XmlPullParser;
35import org.xmlpull.v1.XmlPullParserException;
36import org.xmlpull.v1.XmlSerializer;
37
38import java.io.FileInputStream;
39import java.io.FileNotFoundException;
40import java.io.FileOutputStream;
41import java.io.IOException;
42import java.math.BigDecimal;
43import java.util.ArrayList;
44import java.util.Collections;
45import java.util.HashMap;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070046import java.util.List;
47import java.util.Map;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -070048
49/**
50 * <p>
51 * This class represents a data model for choosing a component for handing a
52 * given {@link Intent}. The model is responsible for querying the system for
53 * activities that can handle the given intent and order found activities
54 * based on historical data of previous choices. The historical data is stored
55 * in an application private file. If a client does not want to have persistent
56 * choice history the file can be omitted, thus the activities will be ordered
57 * based on historical usage for the current session.
58 * <p>
59 * </p>
60 * For each backing history file there is a singleton instance of this class. Thus,
61 * several clients that specify the same history file will share the same model. Note
62 * that if multiple clients are sharing the same model they should implement semantically
63 * equivalent functionality since setting the model intent will change the found
64 * activities and they may be inconsistent with the functionality of some of the clients.
65 * For example, choosing a share activity can be implemented by a single backing
66 * model and two different views for performing the selection. If however, one of the
67 * views is used for sharing but the other for importing, for example, then each
68 * view should be backed by a separate model.
69 * </p>
70 * <p>
71 * The way clients interact with this class is as follows:
72 * </p>
73 * <p>
74 * <pre>
75 * <code>
76 * // Get a model and set it to a couple of clients with semantically similar function.
77 * ActivityChooserModel dataModel =
78 * ActivityChooserModel.get(context, "task_specific_history_file_name.xml");
79 *
80 * ActivityChooserModelClient modelClient1 = getActivityChooserModelClient1();
81 * modelClient1.setActivityChooserModel(dataModel);
82 *
83 * ActivityChooserModelClient modelClient2 = getActivityChooserModelClient2();
84 * modelClient2.setActivityChooserModel(dataModel);
85 *
86 * // Set an intent to choose a an activity for.
87 * dataModel.setIntent(intent);
88 * <pre>
89 * <code>
90 * </p>
91 * <p>
92 * <strong>Note:</strong> This class is thread safe.
93 * </p>
94 *
95 * @hide
96 */
97public class ActivityChooserModel extends DataSetObservable {
98
99 /**
100 * Client that utilizes an {@link ActivityChooserModel}.
101 */
102 public interface ActivityChooserModelClient {
103
104 /**
105 * Sets the {@link ActivityChooserModel}.
106 *
107 * @param dataModel The model.
108 */
109 public void setActivityChooserModel(ActivityChooserModel dataModel);
110 }
111
112 /**
113 * Defines a sorter that is responsible for sorting the activities
114 * based on the provided historical choices and an intent.
115 */
116 public interface ActivitySorter {
117
118 /**
119 * Sorts the <code>activities</code> in descending order of relevance
120 * based on previous history and an intent.
121 *
122 * @param intent The {@link Intent}.
123 * @param activities Activities to be sorted.
124 * @param historicalRecords Historical records.
125 */
126 // This cannot be done by a simple comparator since an Activity weight
127 // is computed from history. Note that Activity implements Comparable.
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700128 public void sort(Intent intent, List<ActivityResolveInfo> activities,
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700129 List<HistoricalRecord> historicalRecords);
130 }
131
132 /**
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700133 * Listener for choosing an activity.
134 */
135 public interface OnChooseActivityListener {
136
137 /**
138 * Called when an activity has been chosen. The client can decide whether
139 * an activity can be chosen and if so the caller of
140 * {@link ActivityChooserModel#chooseActivity(int)} will receive and {@link Intent}
141 * for launching it.
142 * <p>
143 * <strong>Note:</strong> Modifying the intent is not permitted and
144 * any changes to the latter will be ignored.
145 * </p>
146 *
147 * @param host The listener's host model.
148 * @param intent The intent for launching the chosen activity.
149 * @return Whether the intent is handled and should not be delivered to clients.
150 *
151 * @see ActivityChooserModel#chooseActivity(int)
152 */
153 public boolean onChooseActivity(ActivityChooserModel host, Intent intent);
154 }
155
156 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700157 * Flag for selecting debug mode.
158 */
159 private static final boolean DEBUG = false;
160
161 /**
162 * Tag used for logging.
163 */
164 private static final String LOG_TAG = ActivityChooserModel.class.getSimpleName();
165
166 /**
167 * The root tag in the history file.
168 */
169 private static final String TAG_HISTORICAL_RECORDS = "historical-records";
170
171 /**
172 * The tag for a record in the history file.
173 */
174 private static final String TAG_HISTORICAL_RECORD = "historical-record";
175
176 /**
177 * Attribute for the activity.
178 */
179 private static final String ATTRIBUTE_ACTIVITY = "activity";
180
181 /**
182 * Attribute for the choice time.
183 */
184 private static final String ATTRIBUTE_TIME = "time";
185
186 /**
187 * Attribute for the choice weight.
188 */
189 private static final String ATTRIBUTE_WEIGHT = "weight";
190
191 /**
192 * The default name of the choice history file.
193 */
194 public static final String DEFAULT_HISTORY_FILE_NAME =
195 "activity_choser_model_history.xml";
196
197 /**
198 * The default maximal length of the choice history.
199 */
200 public static final int DEFAULT_HISTORY_MAX_LENGTH = 50;
201
202 /**
203 * The amount with which to inflate a chosen activity when set as default.
204 */
205 private static final int DEFAULT_ACTIVITY_INFLATION = 5;
206
207 /**
208 * Default weight for a choice record.
209 */
210 private static final float DEFAULT_HISTORICAL_RECORD_WEIGHT = 1.0f;
211
212 /**
213 * The extension of the history file.
214 */
215 private static final String HISTORY_FILE_EXTENSION = ".xml";
216
217 /**
218 * An invalid item index.
219 */
220 private static final int INVALID_INDEX = -1;
221
222 /**
223 * Lock to guard the model registry.
224 */
225 private static final Object sRegistryLock = new Object();
226
227 /**
228 * This the registry for data models.
229 */
230 private static final Map<String, ActivityChooserModel> sDataModelRegistry =
231 new HashMap<String, ActivityChooserModel>();
232
233 /**
234 * Lock for synchronizing on this instance.
235 */
236 private final Object mInstanceLock = new Object();
237
238 /**
239 * List of activities that can handle the current intent.
240 */
Svetoslav Ganovca858792012-05-05 18:00:26 -0700241 private final List<ActivityResolveInfo> mActivities = new ArrayList<ActivityResolveInfo>();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700242
243 /**
244 * List with historical choice records.
245 */
246 private final List<HistoricalRecord> mHistoricalRecords = new ArrayList<HistoricalRecord>();
247
248 /**
249 * Monitor for added and removed packages.
250 */
251 private final PackageMonitor mPackageMonitor = new DataModelPackageMonitor();
252
253 /**
254 * Context for accessing resources.
255 */
256 private final Context mContext;
257
258 /**
259 * The name of the history file that backs this model.
260 */
261 private final String mHistoryFileName;
262
263 /**
264 * The intent for which a activity is being chosen.
265 */
266 private Intent mIntent;
267
268 /**
269 * The sorter for ordering activities based on intent and past choices.
270 */
271 private ActivitySorter mActivitySorter = new DefaultSorter();
272
273 /**
274 * The maximal length of the choice history.
275 */
276 private int mHistoryMaxSize = DEFAULT_HISTORY_MAX_LENGTH;
277
278 /**
279 * Flag whether choice history can be read. In general many clients can
Svetoslav Ganovca858792012-05-05 18:00:26 -0700280 * share the same data model and {@link #readHistoricalDataIfNeeded()} may be called
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700281 * by arbitrary of them any number of times. Therefore, this class guarantees
282 * that the very first read succeeds and subsequent reads can be performed
Svetoslav Ganovca858792012-05-05 18:00:26 -0700283 * only after a call to {@link #persistHistoricalDataIfNeeded()} followed by change
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700284 * of the share records.
285 */
286 private boolean mCanReadHistoricalData = true;
287
288 /**
289 * Flag whether the choice history was read. This is used to enforce that
Svetoslav Ganovca858792012-05-05 18:00:26 -0700290 * before calling {@link #persistHistoricalDataIfNeeded()} a call to
291 * {@link #persistHistoricalDataIfNeeded()} has been made. This aims to avoid a
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700292 * scenario in which a choice history file exits, it is not read yet and
293 * it is overwritten. Note that always all historical records are read in
294 * full and the file is rewritten. This is necessary since we need to
295 * purge old records that are outside of the sliding window of past choices.
296 */
297 private boolean mReadShareHistoryCalled = false;
298
299 /**
300 * Flag whether the choice records have changed. In general many clients can
Svetoslav Ganovca858792012-05-05 18:00:26 -0700301 * share the same data model and {@link #persistHistoricalDataIfNeeded()} may be called
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700302 * by arbitrary of them any number of times. Therefore, this class guarantees
303 * that choice history will be persisted only if it has changed.
304 */
305 private boolean mHistoricalRecordsChanged = true;
306
307 /**
Svetoslav Ganovca858792012-05-05 18:00:26 -0700308 * Flag whether to reload the activities for the current intent.
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700309 */
Svetoslav Ganovca858792012-05-05 18:00:26 -0700310 private boolean mReloadActivities = false;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700311
312 /**
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700313 * Policy for controlling how the model handles chosen activities.
314 */
315 private OnChooseActivityListener mActivityChoserModelPolicy;
316
317 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700318 * Gets the data model backed by the contents of the provided file with historical data.
319 * Note that only one data model is backed by a given file, thus multiple calls with
320 * the same file name will return the same model instance. If no such instance is present
321 * it is created.
322 * <p>
323 * <strong>Note:</strong> To use the default historical data file clients should explicitly
324 * pass as file name {@link #DEFAULT_HISTORY_FILE_NAME}. If no persistence of the choice
325 * history is desired clients should pass <code>null</code> for the file name. In such
326 * case a new model is returned for each invocation.
327 * </p>
328 *
329 * <p>
330 * <strong>Always use difference historical data files for semantically different actions.
331 * For example, sharing is different from importing.</strong>
332 * </p>
333 *
334 * @param context Context for loading resources.
335 * @param historyFileName File name with choice history, <code>null</code>
336 * if the model should not be backed by a file. In this case the activities
337 * will be ordered only by data from the current session.
338 *
339 * @return The model.
340 */
341 public static ActivityChooserModel get(Context context, String historyFileName) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700342 synchronized (sRegistryLock) {
343 ActivityChooserModel dataModel = sDataModelRegistry.get(historyFileName);
344 if (dataModel == null) {
345 dataModel = new ActivityChooserModel(context, historyFileName);
346 sDataModelRegistry.put(historyFileName, dataModel);
347 }
348 return dataModel;
349 }
350 }
351
352 /**
353 * Creates a new instance.
354 *
355 * @param context Context for loading resources.
356 * @param historyFileName The history XML file.
357 */
358 private ActivityChooserModel(Context context, String historyFileName) {
359 mContext = context.getApplicationContext();
360 if (!TextUtils.isEmpty(historyFileName)
361 && !historyFileName.endsWith(HISTORY_FILE_EXTENSION)) {
362 mHistoryFileName = historyFileName + HISTORY_FILE_EXTENSION;
363 } else {
364 mHistoryFileName = historyFileName;
365 }
Dianne Hackbornd0d75032012-04-19 23:12:09 -0700366 mPackageMonitor.register(mContext, null, true);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700367 }
368
369 /**
370 * Sets an intent for which to choose a activity.
371 * <p>
372 * <strong>Note:</strong> Clients must set only semantically similar
373 * intents for each data model.
374 * <p>
375 *
376 * @param intent The intent.
377 */
378 public void setIntent(Intent intent) {
379 synchronized (mInstanceLock) {
380 if (mIntent == intent) {
381 return;
382 }
383 mIntent = intent;
Svetoslav Ganovca858792012-05-05 18:00:26 -0700384 mReloadActivities = true;
385 ensureConsistentState();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700386 }
387 }
388
389 /**
390 * Gets the intent for which a activity is being chosen.
391 *
392 * @return The intent.
393 */
394 public Intent getIntent() {
395 synchronized (mInstanceLock) {
396 return mIntent;
397 }
398 }
399
400 /**
401 * Gets the number of activities that can handle the intent.
402 *
403 * @return The activity count.
404 *
405 * @see #setIntent(Intent)
406 */
407 public int getActivityCount() {
408 synchronized (mInstanceLock) {
Svetoslav Ganovca858792012-05-05 18:00:26 -0700409 ensureConsistentState();
410 return mActivities.size();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700411 }
412 }
413
414 /**
415 * Gets an activity at a given index.
416 *
417 * @return The activity.
418 *
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700419 * @see ActivityResolveInfo
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700420 * @see #setIntent(Intent)
421 */
422 public ResolveInfo getActivity(int index) {
423 synchronized (mInstanceLock) {
Svetoslav Ganovca858792012-05-05 18:00:26 -0700424 ensureConsistentState();
425 return mActivities.get(index).resolveInfo;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700426 }
427 }
428
429 /**
430 * Gets the index of a the given activity.
431 *
432 * @param activity The activity index.
433 *
434 * @return The index if found, -1 otherwise.
435 */
436 public int getActivityIndex(ResolveInfo activity) {
Svetoslav Ganovca858792012-05-05 18:00:26 -0700437 synchronized (mInstanceLock) {
438 ensureConsistentState();
439 List<ActivityResolveInfo> activities = mActivities;
440 final int activityCount = activities.size();
441 for (int i = 0; i < activityCount; i++) {
442 ActivityResolveInfo currentActivity = activities.get(i);
443 if (currentActivity.resolveInfo == activity) {
444 return i;
445 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700446 }
Svetoslav Ganovca858792012-05-05 18:00:26 -0700447 return INVALID_INDEX;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700448 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700449 }
450
451 /**
452 * Chooses a activity to handle the current intent. This will result in
453 * adding a historical record for that action and construct intent with
454 * its component name set such that it can be immediately started by the
455 * client.
456 * <p>
457 * <strong>Note:</strong> By calling this method the client guarantees
458 * that the returned intent will be started. This intent is returned to
459 * the client solely to let additional customization before the start.
460 * </p>
461 *
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700462 * @return An {@link Intent} for launching the activity or null if the
Svetoslav Ganovabcaeea2012-09-21 11:10:22 -0700463 * policy has consumed the intent or there is not current intent
464 * set via {@link #setIntent(Intent)}.
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700465 *
466 * @see HistoricalRecord
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700467 * @see OnChooseActivityListener
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700468 */
469 public Intent chooseActivity(int index) {
Svetoslav Ganovca858792012-05-05 18:00:26 -0700470 synchronized (mInstanceLock) {
Svetoslav Ganovabcaeea2012-09-21 11:10:22 -0700471 if (mIntent == null) {
472 return null;
473 }
474
Svetoslav Ganovca858792012-05-05 18:00:26 -0700475 ensureConsistentState();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700476
Svetoslav Ganovca858792012-05-05 18:00:26 -0700477 ActivityResolveInfo chosenActivity = mActivities.get(index);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700478
Svetoslav Ganovca858792012-05-05 18:00:26 -0700479 ComponentName chosenName = new ComponentName(
480 chosenActivity.resolveInfo.activityInfo.packageName,
481 chosenActivity.resolveInfo.activityInfo.name);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700482
Svetoslav Ganovca858792012-05-05 18:00:26 -0700483 Intent choiceIntent = new Intent(mIntent);
484 choiceIntent.setComponent(chosenName);
485
486 if (mActivityChoserModelPolicy != null) {
487 // Do not allow the policy to change the intent.
488 Intent choiceIntentCopy = new Intent(choiceIntent);
489 final boolean handled = mActivityChoserModelPolicy.onChooseActivity(this,
490 choiceIntentCopy);
491 if (handled) {
492 return null;
493 }
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700494 }
Svetoslav Ganovca858792012-05-05 18:00:26 -0700495
496 HistoricalRecord historicalRecord = new HistoricalRecord(chosenName,
497 System.currentTimeMillis(), DEFAULT_HISTORICAL_RECORD_WEIGHT);
498 addHisoricalRecord(historicalRecord);
499
500 return choiceIntent;
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700501 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700502 }
503
504 /**
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700505 * Sets the listener for choosing an activity.
506 *
507 * @param listener The listener.
508 */
509 public void setOnChooseActivityListener(OnChooseActivityListener listener) {
Svetoslav Ganovca858792012-05-05 18:00:26 -0700510 synchronized (mInstanceLock) {
511 mActivityChoserModelPolicy = listener;
512 }
Svetoslav Ganov8c6c79f2011-07-29 20:14:09 -0700513 }
514
515 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700516 * Gets the default activity, The default activity is defined as the one
517 * with highest rank i.e. the first one in the list of activities that can
518 * handle the intent.
519 *
520 * @return The default activity, <code>null</code> id not activities.
521 *
522 * @see #getActivity(int)
523 */
524 public ResolveInfo getDefaultActivity() {
525 synchronized (mInstanceLock) {
Svetoslav Ganovca858792012-05-05 18:00:26 -0700526 ensureConsistentState();
527 if (!mActivities.isEmpty()) {
528 return mActivities.get(0).resolveInfo;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700529 }
530 }
531 return null;
532 }
533
534 /**
535 * Sets the default activity. The default activity is set by adding a
536 * historical record with weight high enough that this activity will
537 * become the highest ranked. Such a strategy guarantees that the default
538 * will eventually change if not used. Also the weight of the record for
539 * setting a default is inflated with a constant amount to guarantee that
540 * it will stay as default for awhile.
541 *
542 * @param index The index of the activity to set as default.
543 */
544 public void setDefaultActivity(int index) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700545 synchronized (mInstanceLock) {
Svetoslav Ganovca858792012-05-05 18:00:26 -0700546 ensureConsistentState();
547
548 ActivityResolveInfo newDefaultActivity = mActivities.get(index);
549 ActivityResolveInfo oldDefaultActivity = mActivities.get(0);
550
551 final float weight;
552 if (oldDefaultActivity != null) {
553 // Add a record with weight enough to boost the chosen at the top.
554 weight = oldDefaultActivity.weight - newDefaultActivity.weight
555 + DEFAULT_ACTIVITY_INFLATION;
556 } else {
557 weight = DEFAULT_HISTORICAL_RECORD_WEIGHT;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700558 }
Svetoslav Ganovca858792012-05-05 18:00:26 -0700559
560 ComponentName defaultName = new ComponentName(
561 newDefaultActivity.resolveInfo.activityInfo.packageName,
562 newDefaultActivity.resolveInfo.activityInfo.name);
563 HistoricalRecord historicalRecord = new HistoricalRecord(defaultName,
564 System.currentTimeMillis(), weight);
565 addHisoricalRecord(historicalRecord);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700566 }
567 }
568
569 /**
570 * Persists the history data to the backing file if the latter
Svetoslav Ganovca858792012-05-05 18:00:26 -0700571 * was provided. Calling this method before a call to {@link #readHistoricalDataIfNeeded()}
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700572 * throws an exception. Calling this method more than one without choosing an
573 * activity has not effect.
574 *
575 * @throws IllegalStateException If this method is called before a call to
Svetoslav Ganovca858792012-05-05 18:00:26 -0700576 * {@link #readHistoricalDataIfNeeded()}.
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700577 */
Svetoslav Ganovca858792012-05-05 18:00:26 -0700578 private void persistHistoricalDataIfNeeded() {
579 if (!mReadShareHistoryCalled) {
580 throw new IllegalStateException("No preceding call to #readHistoricalData");
581 }
582 if (!mHistoricalRecordsChanged) {
583 return;
584 }
585 mHistoricalRecordsChanged = false;
586 if (!TextUtils.isEmpty(mHistoryFileName)) {
587 new PersistHistoryAsyncTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR,
588 new ArrayList<HistoricalRecord>(mHistoricalRecords), mHistoryFileName);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700589 }
590 }
591
592 /**
593 * Sets the sorter for ordering activities based on historical data and an intent.
594 *
595 * @param activitySorter The sorter.
596 *
597 * @see ActivitySorter
598 */
599 public void setActivitySorter(ActivitySorter activitySorter) {
600 synchronized (mInstanceLock) {
601 if (mActivitySorter == activitySorter) {
602 return;
603 }
604 mActivitySorter = activitySorter;
Svetoslav Ganovca858792012-05-05 18:00:26 -0700605 if (sortActivitiesIfNeeded()) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700606 notifyChanged();
607 }
608 }
609 }
610
611 /**
612 * Sets the maximal size of the historical data. Defaults to
613 * {@link #DEFAULT_HISTORY_MAX_LENGTH}
614 * <p>
615 * <strong>Note:</strong> Setting this property will immediately
616 * enforce the specified max history size by dropping enough old
617 * historical records to enforce the desired size. Thus, any
618 * records that exceed the history size will be discarded and
619 * irreversibly lost.
620 * </p>
621 *
622 * @param historyMaxSize The max history size.
623 */
624 public void setHistoryMaxSize(int historyMaxSize) {
625 synchronized (mInstanceLock) {
626 if (mHistoryMaxSize == historyMaxSize) {
627 return;
628 }
629 mHistoryMaxSize = historyMaxSize;
Svetoslav Ganovca858792012-05-05 18:00:26 -0700630 pruneExcessiveHistoricalRecordsIfNeeded();
631 if (sortActivitiesIfNeeded()) {
632 notifyChanged();
633 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700634 }
635 }
636
637 /**
638 * Gets the history max size.
639 *
640 * @return The history max size.
641 */
642 public int getHistoryMaxSize() {
643 synchronized (mInstanceLock) {
644 return mHistoryMaxSize;
645 }
646 }
647
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700648 /**
649 * Gets the history size.
650 *
651 * @return The history size.
652 */
653 public int getHistorySize() {
654 synchronized (mInstanceLock) {
Svetoslav Ganovca858792012-05-05 18:00:26 -0700655 ensureConsistentState();
Svetoslav Ganovf2e75402011-09-07 16:38:40 -0700656 return mHistoricalRecords.size();
657 }
658 }
659
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700660 @Override
661 protected void finalize() throws Throwable {
662 super.finalize();
663 mPackageMonitor.unregister();
664 }
665
666 /**
Svetoslav Ganovca858792012-05-05 18:00:26 -0700667 * Ensures the model is in a consistent state which is the
668 * activities for the current intent have been loaded, the
669 * most recent history has been read, and the activities
670 * are sorted.
671 */
672 private void ensureConsistentState() {
673 boolean stateChanged = loadActivitiesIfNeeded();
674 stateChanged |= readHistoricalDataIfNeeded();
675 pruneExcessiveHistoricalRecordsIfNeeded();
676 if (stateChanged) {
677 sortActivitiesIfNeeded();
678 notifyChanged();
679 }
680 }
681
682 /**
683 * Sorts the activities if necessary which is if there is a
684 * sorter, there are some activities to sort, and there is some
685 * historical data.
686 *
687 * @return Whether sorting was performed.
688 */
689 private boolean sortActivitiesIfNeeded() {
690 if (mActivitySorter != null && mIntent != null
691 && !mActivities.isEmpty() && !mHistoricalRecords.isEmpty()) {
692 mActivitySorter.sort(mIntent, mActivities,
693 Collections.unmodifiableList(mHistoricalRecords));
694 return true;
695 }
696 return false;
697 }
698
699 /**
700 * Loads the activities for the current intent if needed which is
701 * if they are not already loaded for the current intent.
702 *
703 * @return Whether loading was performed.
704 */
705 private boolean loadActivitiesIfNeeded() {
706 if (mReloadActivities && mIntent != null) {
707 mReloadActivities = false;
708 mActivities.clear();
709 List<ResolveInfo> resolveInfos = mContext.getPackageManager()
710 .queryIntentActivities(mIntent, 0);
711 final int resolveInfoCount = resolveInfos.size();
712 for (int i = 0; i < resolveInfoCount; i++) {
713 ResolveInfo resolveInfo = resolveInfos.get(i);
Svetoslavbaeabb62013-10-28 15:22:14 -0700714 ActivityInfo activityInfo = resolveInfo.activityInfo;
715 if (ActivityManager.checkComponentPermission(activityInfo.permission,
716 android.os.Process.myUid(), activityInfo.applicationInfo.uid,
717 activityInfo.exported) == PackageManager.PERMISSION_GRANTED) {
718 mActivities.add(new ActivityResolveInfo(resolveInfo));
719 }
Svetoslav Ganovca858792012-05-05 18:00:26 -0700720 }
721 return true;
722 }
723 return false;
724 }
725
726 /**
727 * Reads the historical data if necessary which is it has
728 * changed, there is a history file, and there is not persist
729 * in progress.
730 *
731 * @return Whether reading was performed.
732 */
733 private boolean readHistoricalDataIfNeeded() {
734 if (mCanReadHistoricalData && mHistoricalRecordsChanged &&
735 !TextUtils.isEmpty(mHistoryFileName)) {
736 mCanReadHistoricalData = false;
737 mReadShareHistoryCalled = true;
738 readHistoricalDataImpl();
739 return true;
740 }
741 return false;
742 }
743
744 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700745 * Adds a historical record.
746 *
747 * @param historicalRecord The record to add.
748 * @return True if the record was added.
749 */
750 private boolean addHisoricalRecord(HistoricalRecord historicalRecord) {
Svetoslav Ganovca858792012-05-05 18:00:26 -0700751 final boolean added = mHistoricalRecords.add(historicalRecord);
752 if (added) {
753 mHistoricalRecordsChanged = true;
754 pruneExcessiveHistoricalRecordsIfNeeded();
755 persistHistoricalDataIfNeeded();
756 sortActivitiesIfNeeded();
757 notifyChanged();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700758 }
Svetoslav Ganovca858792012-05-05 18:00:26 -0700759 return added;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700760 }
761
762 /**
Svetoslav Ganovca858792012-05-05 18:00:26 -0700763 * Prunes older excessive records to guarantee maxHistorySize.
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700764 */
Svetoslav Ganovca858792012-05-05 18:00:26 -0700765 private void pruneExcessiveHistoricalRecordsIfNeeded() {
766 final int pruneCount = mHistoricalRecords.size() - mHistoryMaxSize;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700767 if (pruneCount <= 0) {
768 return;
769 }
770 mHistoricalRecordsChanged = true;
771 for (int i = 0; i < pruneCount; i++) {
Svetoslav Ganovca858792012-05-05 18:00:26 -0700772 HistoricalRecord prunedRecord = mHistoricalRecords.remove(0);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700773 if (DEBUG) {
774 Log.i(LOG_TAG, "Pruned: " + prunedRecord);
775 }
776 }
777 }
778
779 /**
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700780 * Represents a record in the history.
781 */
782 public final static class HistoricalRecord {
783
784 /**
785 * The activity name.
786 */
787 public final ComponentName activity;
788
789 /**
790 * The choice time.
791 */
792 public final long time;
793
794 /**
795 * The record weight.
796 */
797 public final float weight;
798
799 /**
800 * Creates a new instance.
801 *
802 * @param activityName The activity component name flattened to string.
803 * @param time The time the activity was chosen.
804 * @param weight The weight of the record.
805 */
806 public HistoricalRecord(String activityName, long time, float weight) {
807 this(ComponentName.unflattenFromString(activityName), time, weight);
808 }
809
810 /**
811 * Creates a new instance.
812 *
813 * @param activityName The activity name.
814 * @param time The time the activity was chosen.
815 * @param weight The weight of the record.
816 */
817 public HistoricalRecord(ComponentName activityName, long time, float weight) {
818 this.activity = activityName;
819 this.time = time;
820 this.weight = weight;
821 }
822
823 @Override
824 public int hashCode() {
825 final int prime = 31;
826 int result = 1;
827 result = prime * result + ((activity == null) ? 0 : activity.hashCode());
828 result = prime * result + (int) (time ^ (time >>> 32));
829 result = prime * result + Float.floatToIntBits(weight);
830 return result;
831 }
832
833 @Override
834 public boolean equals(Object obj) {
835 if (this == obj) {
836 return true;
837 }
838 if (obj == null) {
839 return false;
840 }
841 if (getClass() != obj.getClass()) {
842 return false;
843 }
844 HistoricalRecord other = (HistoricalRecord) obj;
845 if (activity == null) {
846 if (other.activity != null) {
847 return false;
848 }
849 } else if (!activity.equals(other.activity)) {
850 return false;
851 }
852 if (time != other.time) {
853 return false;
854 }
855 if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) {
856 return false;
857 }
858 return true;
859 }
860
861 @Override
862 public String toString() {
863 StringBuilder builder = new StringBuilder();
864 builder.append("[");
865 builder.append("; activity:").append(activity);
866 builder.append("; time:").append(time);
867 builder.append("; weight:").append(new BigDecimal(weight));
868 builder.append("]");
869 return builder.toString();
870 }
871 }
872
873 /**
874 * Represents an activity.
875 */
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700876 public final class ActivityResolveInfo implements Comparable<ActivityResolveInfo> {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700877
878 /**
879 * The {@link ResolveInfo} of the activity.
880 */
881 public final ResolveInfo resolveInfo;
882
883 /**
884 * Weight of the activity. Useful for sorting.
885 */
886 public float weight;
887
888 /**
889 * Creates a new instance.
890 *
891 * @param resolveInfo activity {@link ResolveInfo}.
892 */
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700893 public ActivityResolveInfo(ResolveInfo resolveInfo) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700894 this.resolveInfo = resolveInfo;
895 }
896
897 @Override
898 public int hashCode() {
899 return 31 + Float.floatToIntBits(weight);
900 }
901
902 @Override
903 public boolean equals(Object obj) {
904 if (this == obj) {
905 return true;
906 }
907 if (obj == null) {
908 return false;
909 }
910 if (getClass() != obj.getClass()) {
911 return false;
912 }
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700913 ActivityResolveInfo other = (ActivityResolveInfo) obj;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700914 if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) {
915 return false;
916 }
917 return true;
918 }
919
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700920 public int compareTo(ActivityResolveInfo another) {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700921 return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight);
922 }
923
924 @Override
925 public String toString() {
926 StringBuilder builder = new StringBuilder();
927 builder.append("[");
928 builder.append("resolveInfo:").append(resolveInfo.toString());
929 builder.append("; weight:").append(new BigDecimal(weight));
930 builder.append("]");
931 return builder.toString();
932 }
933 }
934
935 /**
936 * Default activity sorter implementation.
937 */
938 private final class DefaultSorter implements ActivitySorter {
939 private static final float WEIGHT_DECAY_COEFFICIENT = 0.95f;
940
Svetoslav27f592d2013-10-28 18:38:14 -0700941 private final Map<ComponentName, ActivityResolveInfo> mPackageNameToActivityMap =
942 new HashMap<ComponentName, ActivityResolveInfo>();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700943
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700944 public void sort(Intent intent, List<ActivityResolveInfo> activities,
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700945 List<HistoricalRecord> historicalRecords) {
Svetoslav27f592d2013-10-28 18:38:14 -0700946 Map<ComponentName, ActivityResolveInfo> componentNameToActivityMap =
947 mPackageNameToActivityMap;
948 componentNameToActivityMap.clear();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700949
950 final int activityCount = activities.size();
951 for (int i = 0; i < activityCount; i++) {
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700952 ActivityResolveInfo activity = activities.get(i);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700953 activity.weight = 0.0f;
Svetoslav27f592d2013-10-28 18:38:14 -0700954 ComponentName componentName = new ComponentName(
955 activity.resolveInfo.activityInfo.packageName,
956 activity.resolveInfo.activityInfo.name);
957 componentNameToActivityMap.put(componentName, activity);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700958 }
959
960 final int lastShareIndex = historicalRecords.size() - 1;
961 float nextRecordWeight = 1;
962 for (int i = lastShareIndex; i >= 0; i--) {
963 HistoricalRecord historicalRecord = historicalRecords.get(i);
Svetoslav27f592d2013-10-28 18:38:14 -0700964 ComponentName componentName = historicalRecord.activity;
965 ActivityResolveInfo activity = componentNameToActivityMap.get(componentName);
Svetoslav Ganov76559a62011-07-06 17:17:52 -0700966 if (activity != null) {
967 activity.weight += historicalRecord.weight * nextRecordWeight;
968 nextRecordWeight = nextRecordWeight * WEIGHT_DECAY_COEFFICIENT;
969 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700970 }
971
972 Collections.sort(activities);
973
974 if (DEBUG) {
975 for (int i = 0; i < activityCount; i++) {
976 Log.i(LOG_TAG, "Sorted: " + activities.get(i));
977 }
978 }
979 }
980 }
981
Svetoslav Ganovca858792012-05-05 18:00:26 -0700982 private void readHistoricalDataImpl() {
983 FileInputStream fis = null;
984 try {
985 fis = mContext.openFileInput(mHistoryFileName);
986 } catch (FileNotFoundException fnfe) {
987 if (DEBUG) {
988 Log.i(LOG_TAG, "Could not open historical records file: " + mHistoryFileName);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700989 }
Svetoslav Ganovca858792012-05-05 18:00:26 -0700990 return;
991 }
992 try {
993 XmlPullParser parser = Xml.newPullParser();
994 parser.setInput(fis, null);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -0700995
Svetoslav Ganovca858792012-05-05 18:00:26 -0700996 int type = XmlPullParser.START_DOCUMENT;
997 while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) {
998 type = parser.next();
999 }
1000
1001 if (!TAG_HISTORICAL_RECORDS.equals(parser.getName())) {
1002 throw new XmlPullParserException("Share records file does not start with "
1003 + TAG_HISTORICAL_RECORDS + " tag.");
1004 }
1005
1006 List<HistoricalRecord> historicalRecords = mHistoricalRecords;
1007 historicalRecords.clear();
1008
1009 while (true) {
1010 type = parser.next();
1011 if (type == XmlPullParser.END_DOCUMENT) {
1012 break;
1013 }
1014 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
1015 continue;
1016 }
1017 String nodeName = parser.getName();
1018 if (!TAG_HISTORICAL_RECORD.equals(nodeName)) {
1019 throw new XmlPullParserException("Share records file not well-formed.");
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001020 }
1021
Svetoslav Ganovca858792012-05-05 18:00:26 -07001022 String activity = parser.getAttributeValue(null, ATTRIBUTE_ACTIVITY);
1023 final long time =
1024 Long.parseLong(parser.getAttributeValue(null, ATTRIBUTE_TIME));
1025 final float weight =
1026 Float.parseFloat(parser.getAttributeValue(null, ATTRIBUTE_WEIGHT));
1027 HistoricalRecord readRecord = new HistoricalRecord(activity, time, weight);
1028 historicalRecords.add(readRecord);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001029
1030 if (DEBUG) {
Svetoslav Ganovca858792012-05-05 18:00:26 -07001031 Log.i(LOG_TAG, "Read " + readRecord.toString());
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001032 }
Svetoslav Ganovca858792012-05-05 18:00:26 -07001033 }
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001034
Svetoslav Ganovca858792012-05-05 18:00:26 -07001035 if (DEBUG) {
1036 Log.i(LOG_TAG, "Read " + historicalRecords.size() + " historical records.");
1037 }
1038 } catch (XmlPullParserException xppe) {
1039 Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, xppe);
1040 } catch (IOException ioe) {
1041 Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, ioe);
1042 } finally {
1043 if (fis != null) {
1044 try {
1045 fis.close();
1046 } catch (IOException ioe) {
1047 /* ignore */
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001048 }
1049 }
1050 }
1051 }
1052
1053 /**
1054 * Command for persisting the historical records to a file off the UI thread.
1055 */
Svetoslav Ganovca858792012-05-05 18:00:26 -07001056 private final class PersistHistoryAsyncTask extends AsyncTask<Object, Void, Void> {
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001057
Svetoslav Ganovca858792012-05-05 18:00:26 -07001058 @Override
1059 @SuppressWarnings("unchecked")
1060 public Void doInBackground(Object... args) {
1061 List<HistoricalRecord> historicalRecords = (List<HistoricalRecord>) args[0];
1062 String hostoryFileName = (String) args[1];
1063
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001064 FileOutputStream fos = null;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001065
1066 try {
Svetoslav Ganovca858792012-05-05 18:00:26 -07001067 fos = mContext.openFileOutput(hostoryFileName, Context.MODE_PRIVATE);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001068 } catch (FileNotFoundException fnfe) {
Svetoslav Ganovca858792012-05-05 18:00:26 -07001069 Log.e(LOG_TAG, "Error writing historical recrod file: " + hostoryFileName, fnfe);
1070 return null;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001071 }
1072
1073 XmlSerializer serializer = Xml.newSerializer();
1074
1075 try {
1076 serializer.setOutput(fos, null);
1077 serializer.startDocument("UTF-8", true);
1078 serializer.startTag(null, TAG_HISTORICAL_RECORDS);
1079
Svetoslav Ganovca858792012-05-05 18:00:26 -07001080 final int recordCount = historicalRecords.size();
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001081 for (int i = 0; i < recordCount; i++) {
Svetoslav Ganovca858792012-05-05 18:00:26 -07001082 HistoricalRecord record = historicalRecords.remove(0);
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001083 serializer.startTag(null, TAG_HISTORICAL_RECORD);
Svetoslav Ganovca858792012-05-05 18:00:26 -07001084 serializer.attribute(null, ATTRIBUTE_ACTIVITY,
1085 record.activity.flattenToString());
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001086 serializer.attribute(null, ATTRIBUTE_TIME, String.valueOf(record.time));
1087 serializer.attribute(null, ATTRIBUTE_WEIGHT, String.valueOf(record.weight));
1088 serializer.endTag(null, TAG_HISTORICAL_RECORD);
1089 if (DEBUG) {
1090 Log.i(LOG_TAG, "Wrote " + record.toString());
1091 }
1092 }
1093
1094 serializer.endTag(null, TAG_HISTORICAL_RECORDS);
1095 serializer.endDocument();
1096
1097 if (DEBUG) {
1098 Log.i(LOG_TAG, "Wrote " + recordCount + " historical records.");
1099 }
1100 } catch (IllegalArgumentException iae) {
1101 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, iae);
1102 } catch (IllegalStateException ise) {
1103 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ise);
1104 } catch (IOException ioe) {
1105 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ioe);
1106 } finally {
Svetoslav Ganovca858792012-05-05 18:00:26 -07001107 mCanReadHistoricalData = true;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001108 if (fos != null) {
1109 try {
1110 fos.close();
1111 } catch (IOException e) {
1112 /* ignore */
1113 }
1114 }
1115 }
Svetoslav Ganovca858792012-05-05 18:00:26 -07001116 return null;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001117 }
1118 }
1119
1120 /**
1121 * Keeps in sync the historical records and activities with the installed applications.
1122 */
1123 private final class DataModelPackageMonitor extends PackageMonitor {
1124
1125 @Override
Svetoslav Ganovca858792012-05-05 18:00:26 -07001126 public void onSomePackagesChanged() {
1127 mReloadActivities = true;
Svetoslav Ganov51ac0e92011-06-17 13:45:13 -07001128 }
1129 }
1130}