|
1 /* |
|
2 * Copyright (C) 2011 The Android Open Source Project |
|
3 * |
|
4 * Licensed under the Apache License, Version 2.0 (the "License"); |
|
5 * you may not use this file except in compliance with the License. |
|
6 * You may obtain a copy of the License at |
|
7 * |
|
8 * http://www.apache.org/licenses/LICENSE-2.0 |
|
9 * |
|
10 * Unless required by applicable law or agreed to in writing, software |
|
11 * distributed under the License is distributed on an "AS IS" BASIS, |
|
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
13 * See the License for the specific language governing permissions and |
|
14 * limitations under the License. |
|
15 */ |
|
16 |
|
17 /** |
|
18 * Mozilla: Changing the package. |
|
19 */ |
|
20 //package android.widget; |
|
21 package org.mozilla.gecko.widget; |
|
22 |
|
23 // Mozilla: New import |
|
24 import org.mozilla.gecko.Distribution; |
|
25 import org.mozilla.gecko.GeckoProfile; |
|
26 import java.io.File; |
|
27 |
|
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; |
|
39 |
|
40 /** |
|
41 * Mozilla: Unused import. |
|
42 */ |
|
43 //import com.android.internal.content.PackageMonitor; |
|
44 |
|
45 import org.xmlpull.v1.XmlPullParser; |
|
46 import org.xmlpull.v1.XmlPullParserException; |
|
47 import org.xmlpull.v1.XmlSerializer; |
|
48 |
|
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; |
|
60 |
|
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 { |
|
110 |
|
111 /** |
|
112 * Client that utilizes an {@link ActivityChooserModel}. |
|
113 */ |
|
114 public interface ActivityChooserModelClient { |
|
115 |
|
116 /** |
|
117 * Sets the {@link ActivityChooserModel}. |
|
118 * |
|
119 * @param dataModel The model. |
|
120 */ |
|
121 public void setActivityChooserModel(ActivityChooserModel dataModel); |
|
122 } |
|
123 |
|
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 { |
|
129 |
|
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 } |
|
143 |
|
144 /** |
|
145 * Listener for choosing an activity. |
|
146 */ |
|
147 public interface OnChooseActivityListener { |
|
148 |
|
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 } |
|
167 |
|
168 /** |
|
169 * Flag for selecting debug mode. |
|
170 */ |
|
171 private static final boolean DEBUG = false; |
|
172 |
|
173 /** |
|
174 * Tag used for logging. |
|
175 */ |
|
176 private static final String LOG_TAG = ActivityChooserModel.class.getSimpleName(); |
|
177 |
|
178 /** |
|
179 * The root tag in the history file. |
|
180 */ |
|
181 private static final String TAG_HISTORICAL_RECORDS = "historical-records"; |
|
182 |
|
183 /** |
|
184 * The tag for a record in the history file. |
|
185 */ |
|
186 private static final String TAG_HISTORICAL_RECORD = "historical-record"; |
|
187 |
|
188 /** |
|
189 * Attribute for the activity. |
|
190 */ |
|
191 private static final String ATTRIBUTE_ACTIVITY = "activity"; |
|
192 |
|
193 /** |
|
194 * Attribute for the choice time. |
|
195 */ |
|
196 private static final String ATTRIBUTE_TIME = "time"; |
|
197 |
|
198 /** |
|
199 * Attribute for the choice weight. |
|
200 */ |
|
201 private static final String ATTRIBUTE_WEIGHT = "weight"; |
|
202 |
|
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"; |
|
208 |
|
209 /** |
|
210 * The default maximal length of the choice history. |
|
211 */ |
|
212 public static final int DEFAULT_HISTORY_MAX_LENGTH = 50; |
|
213 |
|
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; |
|
218 |
|
219 /** |
|
220 * Default weight for a choice record. |
|
221 */ |
|
222 private static final float DEFAULT_HISTORICAL_RECORD_WEIGHT = 1.0f; |
|
223 |
|
224 /** |
|
225 * The extension of the history file. |
|
226 */ |
|
227 private static final String HISTORY_FILE_EXTENSION = ".xml"; |
|
228 |
|
229 /** |
|
230 * An invalid item index. |
|
231 */ |
|
232 private static final int INVALID_INDEX = -1; |
|
233 |
|
234 /** |
|
235 * Lock to guard the model registry. |
|
236 */ |
|
237 private static final Object sRegistryLock = new Object(); |
|
238 |
|
239 /** |
|
240 * This the registry for data models. |
|
241 */ |
|
242 private static final Map<String, ActivityChooserModel> sDataModelRegistry = |
|
243 new HashMap<String, ActivityChooserModel>(); |
|
244 |
|
245 /** |
|
246 * Lock for synchronizing on this instance. |
|
247 */ |
|
248 private final Object mInstanceLock = new Object(); |
|
249 |
|
250 /** |
|
251 * List of activities that can handle the current intent. |
|
252 */ |
|
253 private final List<ActivityResolveInfo> mActivities = new ArrayList<ActivityResolveInfo>(); |
|
254 |
|
255 /** |
|
256 * List with historical choice records. |
|
257 */ |
|
258 private final List<HistoricalRecord> mHistoricalRecords = new ArrayList<HistoricalRecord>(); |
|
259 |
|
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(); |
|
267 |
|
268 /** |
|
269 * Context for accessing resources. |
|
270 */ |
|
271 private final Context mContext; |
|
272 |
|
273 /** |
|
274 * The name of the history file that backs this model. |
|
275 */ |
|
276 private final String mHistoryFileName; |
|
277 |
|
278 /** |
|
279 * The intent for which a activity is being chosen. |
|
280 */ |
|
281 private Intent mIntent; |
|
282 |
|
283 /** |
|
284 * The sorter for ordering activities based on intent and past choices. |
|
285 */ |
|
286 private ActivitySorter mActivitySorter = new DefaultSorter(); |
|
287 |
|
288 /** |
|
289 * The maximal length of the choice history. |
|
290 */ |
|
291 private int mHistoryMaxSize = DEFAULT_HISTORY_MAX_LENGTH; |
|
292 |
|
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; |
|
302 |
|
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; |
|
313 |
|
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; |
|
321 |
|
322 /** |
|
323 * Flag whether to reload the activities for the current intent. |
|
324 */ |
|
325 private boolean mReloadActivities = false; |
|
326 |
|
327 /** |
|
328 * Policy for controlling how the model handles chosen activities. |
|
329 */ |
|
330 private OnChooseActivityListener mActivityChoserModelPolicy; |
|
331 |
|
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 } |
|
366 |
|
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 } |
|
381 |
|
382 /** |
|
383 * Mozilla: Uses modified receiver |
|
384 */ |
|
385 mPackageMonitor.register(mContext); |
|
386 } |
|
387 |
|
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 } |
|
407 |
|
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 } |
|
418 |
|
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 } |
|
432 |
|
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 } |
|
447 |
|
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 } |
|
469 |
|
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 } |
|
493 |
|
494 ensureConsistentState(); |
|
495 |
|
496 ActivityResolveInfo chosenActivity = mActivities.get(index); |
|
497 |
|
498 ComponentName chosenName = new ComponentName( |
|
499 chosenActivity.resolveInfo.activityInfo.packageName, |
|
500 chosenActivity.resolveInfo.activityInfo.name); |
|
501 |
|
502 Intent choiceIntent = new Intent(mIntent); |
|
503 choiceIntent.setComponent(chosenName); |
|
504 |
|
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 } |
|
514 |
|
515 HistoricalRecord historicalRecord = new HistoricalRecord(chosenName, |
|
516 System.currentTimeMillis(), DEFAULT_HISTORICAL_RECORD_WEIGHT); |
|
517 addHistoricalRecord(historicalRecord); |
|
518 |
|
519 return choiceIntent; |
|
520 } |
|
521 } |
|
522 |
|
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 } |
|
533 |
|
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 } |
|
552 |
|
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(); |
|
566 |
|
567 ActivityResolveInfo newDefaultActivity = mActivities.get(index); |
|
568 ActivityResolveInfo oldDefaultActivity = mActivities.get(0); |
|
569 |
|
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 } |
|
578 |
|
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 } |
|
587 |
|
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 } |
|
612 |
|
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 } |
|
631 |
|
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 } |
|
657 |
|
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 } |
|
668 |
|
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 } |
|
680 |
|
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 } |
|
694 |
|
695 @Override |
|
696 protected void finalize() throws Throwable { |
|
697 super.finalize(); |
|
698 |
|
699 /** |
|
700 * Mozilla: Not needed for the application. |
|
701 */ |
|
702 mPackageMonitor.unregister(); |
|
703 } |
|
704 |
|
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 } |
|
720 |
|
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 } |
|
737 |
|
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 } |
|
759 |
|
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 } |
|
777 |
|
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 } |
|
795 |
|
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; |
|
804 |
|
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 } |
|
812 |
|
813 if (removed) { |
|
814 mHistoricalRecordsChanged = true; |
|
815 pruneExcessiveHistoricalRecordsIfNeeded(); |
|
816 persistHistoricalDataIfNeeded(); |
|
817 sortActivitiesIfNeeded(); |
|
818 notifyChanged(); |
|
819 } |
|
820 |
|
821 return removed; |
|
822 } |
|
823 |
|
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 } |
|
840 |
|
841 /** |
|
842 * Represents a record in the history. |
|
843 */ |
|
844 public final static class HistoricalRecord { |
|
845 |
|
846 /** |
|
847 * The activity name. |
|
848 */ |
|
849 public final ComponentName activity; |
|
850 |
|
851 /** |
|
852 * The choice time. |
|
853 */ |
|
854 public final long time; |
|
855 |
|
856 /** |
|
857 * The record weight. |
|
858 */ |
|
859 public final float weight; |
|
860 |
|
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 } |
|
871 |
|
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 } |
|
884 |
|
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 } |
|
894 |
|
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 } |
|
922 |
|
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 } |
|
934 |
|
935 /** |
|
936 * Represents an activity. |
|
937 */ |
|
938 public final class ActivityResolveInfo implements Comparable<ActivityResolveInfo> { |
|
939 |
|
940 /** |
|
941 * The {@link ResolveInfo} of the activity. |
|
942 */ |
|
943 public final ResolveInfo resolveInfo; |
|
944 |
|
945 /** |
|
946 * Weight of the activity. Useful for sorting. |
|
947 */ |
|
948 public float weight; |
|
949 |
|
950 /** |
|
951 * Creates a new instance. |
|
952 * |
|
953 * @param resolveInfo activity {@link ResolveInfo}. |
|
954 */ |
|
955 public ActivityResolveInfo(ResolveInfo resolveInfo) { |
|
956 this.resolveInfo = resolveInfo; |
|
957 } |
|
958 |
|
959 @Override |
|
960 public int hashCode() { |
|
961 return 31 + Float.floatToIntBits(weight); |
|
962 } |
|
963 |
|
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 } |
|
981 |
|
982 public int compareTo(ActivityResolveInfo another) { |
|
983 return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight); |
|
984 } |
|
985 |
|
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 } |
|
996 |
|
997 /** |
|
998 * Default activity sorter implementation. |
|
999 */ |
|
1000 private final class DefaultSorter implements ActivitySorter { |
|
1001 private static final float WEIGHT_DECAY_COEFFICIENT = 0.95f; |
|
1002 |
|
1003 private final Map<String, ActivityResolveInfo> mPackageNameToActivityMap = |
|
1004 new HashMap<String, ActivityResolveInfo>(); |
|
1005 |
|
1006 public void sort(Intent intent, List<ActivityResolveInfo> activities, |
|
1007 List<HistoricalRecord> historicalRecords) { |
|
1008 Map<String, ActivityResolveInfo> packageNameToActivityMap = |
|
1009 mPackageNameToActivityMap; |
|
1010 packageNameToActivityMap.clear(); |
|
1011 |
|
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; |
|
1016 |
|
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 } |
|
1024 |
|
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 } |
|
1036 |
|
1037 Collections.sort(activities); |
|
1038 |
|
1039 if (DEBUG) { |
|
1040 for (int i = 0; i < activityCount; i++) { |
|
1041 Log.i(LOG_TAG, "Sorted: " + activities.get(i)); |
|
1042 } |
|
1043 } |
|
1044 } |
|
1045 } |
|
1046 |
|
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 } |
|
1079 |
|
1080 try { |
|
1081 XmlPullParser parser = Xml.newPullParser(); |
|
1082 parser.setInput(fis, null); |
|
1083 |
|
1084 int type = XmlPullParser.START_DOCUMENT; |
|
1085 while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) { |
|
1086 type = parser.next(); |
|
1087 } |
|
1088 |
|
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 } |
|
1093 |
|
1094 List<HistoricalRecord> historicalRecords = mHistoricalRecords; |
|
1095 historicalRecords.clear(); |
|
1096 |
|
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 } |
|
1109 |
|
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); |
|
1117 |
|
1118 if (DEBUG) { |
|
1119 Log.i(LOG_TAG, "Read " + readRecord.toString()); |
|
1120 } |
|
1121 } |
|
1122 |
|
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 } |
|
1140 |
|
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> { |
|
1145 |
|
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]; |
|
1151 |
|
1152 FileOutputStream fos = null; |
|
1153 |
|
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 } |
|
1163 |
|
1164 XmlSerializer serializer = Xml.newSerializer(); |
|
1165 |
|
1166 try { |
|
1167 serializer.setOutput(fos, null); |
|
1168 serializer.startDocument("UTF-8", true); |
|
1169 serializer.startTag(null, TAG_HISTORICAL_RECORDS); |
|
1170 |
|
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 } |
|
1184 |
|
1185 serializer.endTag(null, TAG_HISTORICAL_RECORDS); |
|
1186 serializer.endDocument(); |
|
1187 |
|
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 } |
|
1210 |
|
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; |
|
1220 |
|
1221 public DataModelPackageMonitor() { } |
|
1222 |
|
1223 public void register(Context context) { |
|
1224 mContext = context; |
|
1225 |
|
1226 String[] intents = new String[] { |
|
1227 Intent.ACTION_PACKAGE_REMOVED, |
|
1228 Intent.ACTION_PACKAGE_ADDED, |
|
1229 Intent.ACTION_PACKAGE_CHANGED |
|
1230 }; |
|
1231 |
|
1232 for (String intent : intents) { |
|
1233 IntentFilter removeFilter = new IntentFilter(intent); |
|
1234 removeFilter.addDataScheme("package"); |
|
1235 context.registerReceiver(this, removeFilter); |
|
1236 } |
|
1237 } |
|
1238 |
|
1239 public void unregister() { |
|
1240 mContext.unregisterReceiver(this); |
|
1241 mContext = null; |
|
1242 } |
|
1243 |
|
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 } |
|
1251 |
|
1252 mReloadActivities = true; |
|
1253 } |
|
1254 } |
|
1255 } |
|
1256 |