1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/widget/ActivityChooserModel.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1256 @@ 1.4 +/* 1.5 + * Copyright (C) 2011 The Android Open Source Project 1.6 + * 1.7 + * Licensed under the Apache License, Version 2.0 (the "License"); 1.8 + * you may not use this file except in compliance with the License. 1.9 + * You may obtain a copy of the License at 1.10 + * 1.11 + * http://www.apache.org/licenses/LICENSE-2.0 1.12 + * 1.13 + * Unless required by applicable law or agreed to in writing, software 1.14 + * distributed under the License is distributed on an "AS IS" BASIS, 1.15 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1.16 + * See the License for the specific language governing permissions and 1.17 + * limitations under the License. 1.18 + */ 1.19 + 1.20 +/** 1.21 + * Mozilla: Changing the package. 1.22 + */ 1.23 +//package android.widget; 1.24 +package org.mozilla.gecko.widget; 1.25 + 1.26 +// Mozilla: New import 1.27 +import org.mozilla.gecko.Distribution; 1.28 +import org.mozilla.gecko.GeckoProfile; 1.29 +import java.io.File; 1.30 + 1.31 +import android.content.BroadcastReceiver; 1.32 +import android.content.ComponentName; 1.33 +import android.content.Context; 1.34 +import android.content.Intent; 1.35 +import android.content.IntentFilter; 1.36 +import android.content.pm.ResolveInfo; 1.37 +import android.database.DataSetObservable; 1.38 +import android.os.AsyncTask; 1.39 +import android.text.TextUtils; 1.40 +import android.util.Log; 1.41 +import android.util.Xml; 1.42 + 1.43 +/** 1.44 + * Mozilla: Unused import. 1.45 + */ 1.46 +//import com.android.internal.content.PackageMonitor; 1.47 + 1.48 +import org.xmlpull.v1.XmlPullParser; 1.49 +import org.xmlpull.v1.XmlPullParserException; 1.50 +import org.xmlpull.v1.XmlSerializer; 1.51 + 1.52 +import java.io.FileInputStream; 1.53 +import java.io.FileNotFoundException; 1.54 +import java.io.FileOutputStream; 1.55 +import java.io.IOException; 1.56 +import java.math.BigDecimal; 1.57 +import java.util.ArrayList; 1.58 +import java.util.Collections; 1.59 +import java.util.HashMap; 1.60 +import java.util.Iterator; 1.61 +import java.util.List; 1.62 +import java.util.Map; 1.63 + 1.64 +/** 1.65 + * <p> 1.66 + * This class represents a data model for choosing a component for handing a 1.67 + * given {@link Intent}. The model is responsible for querying the system for 1.68 + * activities that can handle the given intent and order found activities 1.69 + * based on historical data of previous choices. The historical data is stored 1.70 + * in an application private file. If a client does not want to have persistent 1.71 + * choice history the file can be omitted, thus the activities will be ordered 1.72 + * based on historical usage for the current session. 1.73 + * <p> 1.74 + * </p> 1.75 + * For each backing history file there is a singleton instance of this class. Thus, 1.76 + * several clients that specify the same history file will share the same model. Note 1.77 + * that if multiple clients are sharing the same model they should implement semantically 1.78 + * equivalent functionality since setting the model intent will change the found 1.79 + * activities and they may be inconsistent with the functionality of some of the clients. 1.80 + * For example, choosing a share activity can be implemented by a single backing 1.81 + * model and two different views for performing the selection. If however, one of the 1.82 + * views is used for sharing but the other for importing, for example, then each 1.83 + * view should be backed by a separate model. 1.84 + * </p> 1.85 + * <p> 1.86 + * The way clients interact with this class is as follows: 1.87 + * </p> 1.88 + * <p> 1.89 + * <pre> 1.90 + * <code> 1.91 + * // Get a model and set it to a couple of clients with semantically similar function. 1.92 + * ActivityChooserModel dataModel = 1.93 + * ActivityChooserModel.get(context, "task_specific_history_file_name.xml"); 1.94 + * 1.95 + * ActivityChooserModelClient modelClient1 = getActivityChooserModelClient1(); 1.96 + * modelClient1.setActivityChooserModel(dataModel); 1.97 + * 1.98 + * ActivityChooserModelClient modelClient2 = getActivityChooserModelClient2(); 1.99 + * modelClient2.setActivityChooserModel(dataModel); 1.100 + * 1.101 + * // Set an intent to choose a an activity for. 1.102 + * dataModel.setIntent(intent); 1.103 + * <pre> 1.104 + * <code> 1.105 + * </p> 1.106 + * <p> 1.107 + * <strong>Note:</strong> This class is thread safe. 1.108 + * </p> 1.109 + * 1.110 + * @hide 1.111 + */ 1.112 +public class ActivityChooserModel extends DataSetObservable { 1.113 + 1.114 + /** 1.115 + * Client that utilizes an {@link ActivityChooserModel}. 1.116 + */ 1.117 + public interface ActivityChooserModelClient { 1.118 + 1.119 + /** 1.120 + * Sets the {@link ActivityChooserModel}. 1.121 + * 1.122 + * @param dataModel The model. 1.123 + */ 1.124 + public void setActivityChooserModel(ActivityChooserModel dataModel); 1.125 + } 1.126 + 1.127 + /** 1.128 + * Defines a sorter that is responsible for sorting the activities 1.129 + * based on the provided historical choices and an intent. 1.130 + */ 1.131 + public interface ActivitySorter { 1.132 + 1.133 + /** 1.134 + * Sorts the <code>activities</code> in descending order of relevance 1.135 + * based on previous history and an intent. 1.136 + * 1.137 + * @param intent The {@link Intent}. 1.138 + * @param activities Activities to be sorted. 1.139 + * @param historicalRecords Historical records. 1.140 + */ 1.141 + // This cannot be done by a simple comparator since an Activity weight 1.142 + // is computed from history. Note that Activity implements Comparable. 1.143 + public void sort(Intent intent, List<ActivityResolveInfo> activities, 1.144 + List<HistoricalRecord> historicalRecords); 1.145 + } 1.146 + 1.147 + /** 1.148 + * Listener for choosing an activity. 1.149 + */ 1.150 + public interface OnChooseActivityListener { 1.151 + 1.152 + /** 1.153 + * Called when an activity has been chosen. The client can decide whether 1.154 + * an activity can be chosen and if so the caller of 1.155 + * {@link ActivityChooserModel#chooseActivity(int)} will receive and {@link Intent} 1.156 + * for launching it. 1.157 + * <p> 1.158 + * <strong>Note:</strong> Modifying the intent is not permitted and 1.159 + * any changes to the latter will be ignored. 1.160 + * </p> 1.161 + * 1.162 + * @param host The listener's host model. 1.163 + * @param intent The intent for launching the chosen activity. 1.164 + * @return Whether the intent is handled and should not be delivered to clients. 1.165 + * 1.166 + * @see ActivityChooserModel#chooseActivity(int) 1.167 + */ 1.168 + public boolean onChooseActivity(ActivityChooserModel host, Intent intent); 1.169 + } 1.170 + 1.171 + /** 1.172 + * Flag for selecting debug mode. 1.173 + */ 1.174 + private static final boolean DEBUG = false; 1.175 + 1.176 + /** 1.177 + * Tag used for logging. 1.178 + */ 1.179 + private static final String LOG_TAG = ActivityChooserModel.class.getSimpleName(); 1.180 + 1.181 + /** 1.182 + * The root tag in the history file. 1.183 + */ 1.184 + private static final String TAG_HISTORICAL_RECORDS = "historical-records"; 1.185 + 1.186 + /** 1.187 + * The tag for a record in the history file. 1.188 + */ 1.189 + private static final String TAG_HISTORICAL_RECORD = "historical-record"; 1.190 + 1.191 + /** 1.192 + * Attribute for the activity. 1.193 + */ 1.194 + private static final String ATTRIBUTE_ACTIVITY = "activity"; 1.195 + 1.196 + /** 1.197 + * Attribute for the choice time. 1.198 + */ 1.199 + private static final String ATTRIBUTE_TIME = "time"; 1.200 + 1.201 + /** 1.202 + * Attribute for the choice weight. 1.203 + */ 1.204 + private static final String ATTRIBUTE_WEIGHT = "weight"; 1.205 + 1.206 + /** 1.207 + * The default name of the choice history file. 1.208 + */ 1.209 + public static final String DEFAULT_HISTORY_FILE_NAME = 1.210 + "activity_choser_model_history.xml"; 1.211 + 1.212 + /** 1.213 + * The default maximal length of the choice history. 1.214 + */ 1.215 + public static final int DEFAULT_HISTORY_MAX_LENGTH = 50; 1.216 + 1.217 + /** 1.218 + * The amount with which to inflate a chosen activity when set as default. 1.219 + */ 1.220 + private static final int DEFAULT_ACTIVITY_INFLATION = 5; 1.221 + 1.222 + /** 1.223 + * Default weight for a choice record. 1.224 + */ 1.225 + private static final float DEFAULT_HISTORICAL_RECORD_WEIGHT = 1.0f; 1.226 + 1.227 + /** 1.228 + * The extension of the history file. 1.229 + */ 1.230 + private static final String HISTORY_FILE_EXTENSION = ".xml"; 1.231 + 1.232 + /** 1.233 + * An invalid item index. 1.234 + */ 1.235 + private static final int INVALID_INDEX = -1; 1.236 + 1.237 + /** 1.238 + * Lock to guard the model registry. 1.239 + */ 1.240 + private static final Object sRegistryLock = new Object(); 1.241 + 1.242 + /** 1.243 + * This the registry for data models. 1.244 + */ 1.245 + private static final Map<String, ActivityChooserModel> sDataModelRegistry = 1.246 + new HashMap<String, ActivityChooserModel>(); 1.247 + 1.248 + /** 1.249 + * Lock for synchronizing on this instance. 1.250 + */ 1.251 + private final Object mInstanceLock = new Object(); 1.252 + 1.253 + /** 1.254 + * List of activities that can handle the current intent. 1.255 + */ 1.256 + private final List<ActivityResolveInfo> mActivities = new ArrayList<ActivityResolveInfo>(); 1.257 + 1.258 + /** 1.259 + * List with historical choice records. 1.260 + */ 1.261 + private final List<HistoricalRecord> mHistoricalRecords = new ArrayList<HistoricalRecord>(); 1.262 + 1.263 + /** 1.264 + * Monitor for added and removed packages. 1.265 + */ 1.266 + /** 1.267 + * Mozilla: Converted from a PackageMonitor to a DataModelPackageMonitor to avoid importing a new class. 1.268 + */ 1.269 + private final DataModelPackageMonitor mPackageMonitor = new DataModelPackageMonitor(); 1.270 + 1.271 + /** 1.272 + * Context for accessing resources. 1.273 + */ 1.274 + private final Context mContext; 1.275 + 1.276 + /** 1.277 + * The name of the history file that backs this model. 1.278 + */ 1.279 + private final String mHistoryFileName; 1.280 + 1.281 + /** 1.282 + * The intent for which a activity is being chosen. 1.283 + */ 1.284 + private Intent mIntent; 1.285 + 1.286 + /** 1.287 + * The sorter for ordering activities based on intent and past choices. 1.288 + */ 1.289 + private ActivitySorter mActivitySorter = new DefaultSorter(); 1.290 + 1.291 + /** 1.292 + * The maximal length of the choice history. 1.293 + */ 1.294 + private int mHistoryMaxSize = DEFAULT_HISTORY_MAX_LENGTH; 1.295 + 1.296 + /** 1.297 + * Flag whether choice history can be read. In general many clients can 1.298 + * share the same data model and {@link #readHistoricalDataIfNeeded()} may be called 1.299 + * by arbitrary of them any number of times. Therefore, this class guarantees 1.300 + * that the very first read succeeds and subsequent reads can be performed 1.301 + * only after a call to {@link #persistHistoricalDataIfNeeded()} followed by change 1.302 + * of the share records. 1.303 + */ 1.304 + private boolean mCanReadHistoricalData = true; 1.305 + 1.306 + /** 1.307 + * Flag whether the choice history was read. This is used to enforce that 1.308 + * before calling {@link #persistHistoricalDataIfNeeded()} a call to 1.309 + * {@link #persistHistoricalDataIfNeeded()} has been made. This aims to avoid a 1.310 + * scenario in which a choice history file exits, it is not read yet and 1.311 + * it is overwritten. Note that always all historical records are read in 1.312 + * full and the file is rewritten. This is necessary since we need to 1.313 + * purge old records that are outside of the sliding window of past choices. 1.314 + */ 1.315 + private boolean mReadShareHistoryCalled = false; 1.316 + 1.317 + /** 1.318 + * Flag whether the choice records have changed. In general many clients can 1.319 + * share the same data model and {@link #persistHistoricalDataIfNeeded()} may be called 1.320 + * by arbitrary of them any number of times. Therefore, this class guarantees 1.321 + * that choice history will be persisted only if it has changed. 1.322 + */ 1.323 + private boolean mHistoricalRecordsChanged = true; 1.324 + 1.325 + /** 1.326 + * Flag whether to reload the activities for the current intent. 1.327 + */ 1.328 + private boolean mReloadActivities = false; 1.329 + 1.330 + /** 1.331 + * Policy for controlling how the model handles chosen activities. 1.332 + */ 1.333 + private OnChooseActivityListener mActivityChoserModelPolicy; 1.334 + 1.335 + /** 1.336 + * Gets the data model backed by the contents of the provided file with historical data. 1.337 + * Note that only one data model is backed by a given file, thus multiple calls with 1.338 + * the same file name will return the same model instance. If no such instance is present 1.339 + * it is created. 1.340 + * <p> 1.341 + * <strong>Note:</strong> To use the default historical data file clients should explicitly 1.342 + * pass as file name {@link #DEFAULT_HISTORY_FILE_NAME}. If no persistence of the choice 1.343 + * history is desired clients should pass <code>null</code> for the file name. In such 1.344 + * case a new model is returned for each invocation. 1.345 + * </p> 1.346 + * 1.347 + * <p> 1.348 + * <strong>Always use difference historical data files for semantically different actions. 1.349 + * For example, sharing is different from importing.</strong> 1.350 + * </p> 1.351 + * 1.352 + * @param context Context for loading resources. 1.353 + * @param historyFileName File name with choice history, <code>null</code> 1.354 + * if the model should not be backed by a file. In this case the activities 1.355 + * will be ordered only by data from the current session. 1.356 + * 1.357 + * @return The model. 1.358 + */ 1.359 + public static ActivityChooserModel get(Context context, String historyFileName) { 1.360 + synchronized (sRegistryLock) { 1.361 + ActivityChooserModel dataModel = sDataModelRegistry.get(historyFileName); 1.362 + if (dataModel == null) { 1.363 + dataModel = new ActivityChooserModel(context, historyFileName); 1.364 + sDataModelRegistry.put(historyFileName, dataModel); 1.365 + } 1.366 + return dataModel; 1.367 + } 1.368 + } 1.369 + 1.370 + /** 1.371 + * Creates a new instance. 1.372 + * 1.373 + * @param context Context for loading resources. 1.374 + * @param historyFileName The history XML file. 1.375 + */ 1.376 + private ActivityChooserModel(Context context, String historyFileName) { 1.377 + mContext = context.getApplicationContext(); 1.378 + if (!TextUtils.isEmpty(historyFileName) 1.379 + && !historyFileName.endsWith(HISTORY_FILE_EXTENSION)) { 1.380 + mHistoryFileName = historyFileName + HISTORY_FILE_EXTENSION; 1.381 + } else { 1.382 + mHistoryFileName = historyFileName; 1.383 + } 1.384 + 1.385 + /** 1.386 + * Mozilla: Uses modified receiver 1.387 + */ 1.388 + mPackageMonitor.register(mContext); 1.389 + } 1.390 + 1.391 + /** 1.392 + * Sets an intent for which to choose a activity. 1.393 + * <p> 1.394 + * <strong>Note:</strong> Clients must set only semantically similar 1.395 + * intents for each data model. 1.396 + * <p> 1.397 + * 1.398 + * @param intent The intent. 1.399 + */ 1.400 + public void setIntent(Intent intent) { 1.401 + synchronized (mInstanceLock) { 1.402 + if (mIntent == intent) { 1.403 + return; 1.404 + } 1.405 + mIntent = intent; 1.406 + mReloadActivities = true; 1.407 + ensureConsistentState(); 1.408 + } 1.409 + } 1.410 + 1.411 + /** 1.412 + * Gets the intent for which a activity is being chosen. 1.413 + * 1.414 + * @return The intent. 1.415 + */ 1.416 + public Intent getIntent() { 1.417 + synchronized (mInstanceLock) { 1.418 + return mIntent; 1.419 + } 1.420 + } 1.421 + 1.422 + /** 1.423 + * Gets the number of activities that can handle the intent. 1.424 + * 1.425 + * @return The activity count. 1.426 + * 1.427 + * @see #setIntent(Intent) 1.428 + */ 1.429 + public int getActivityCount() { 1.430 + synchronized (mInstanceLock) { 1.431 + ensureConsistentState(); 1.432 + return mActivities.size(); 1.433 + } 1.434 + } 1.435 + 1.436 + /** 1.437 + * Gets an activity at a given index. 1.438 + * 1.439 + * @return The activity. 1.440 + * 1.441 + * @see ActivityResolveInfo 1.442 + * @see #setIntent(Intent) 1.443 + */ 1.444 + public ResolveInfo getActivity(int index) { 1.445 + synchronized (mInstanceLock) { 1.446 + ensureConsistentState(); 1.447 + return mActivities.get(index).resolveInfo; 1.448 + } 1.449 + } 1.450 + 1.451 + /** 1.452 + * Gets the index of a the given activity. 1.453 + * 1.454 + * @param activity The activity index. 1.455 + * 1.456 + * @return The index if found, -1 otherwise. 1.457 + */ 1.458 + public int getActivityIndex(ResolveInfo activity) { 1.459 + synchronized (mInstanceLock) { 1.460 + ensureConsistentState(); 1.461 + List<ActivityResolveInfo> activities = mActivities; 1.462 + final int activityCount = activities.size(); 1.463 + for (int i = 0; i < activityCount; i++) { 1.464 + ActivityResolveInfo currentActivity = activities.get(i); 1.465 + if (currentActivity.resolveInfo == activity) { 1.466 + return i; 1.467 + } 1.468 + } 1.469 + return INVALID_INDEX; 1.470 + } 1.471 + } 1.472 + 1.473 + /** 1.474 + * Chooses a activity to handle the current intent. This will result in 1.475 + * adding a historical record for that action and construct intent with 1.476 + * its component name set such that it can be immediately started by the 1.477 + * client. 1.478 + * <p> 1.479 + * <strong>Note:</strong> By calling this method the client guarantees 1.480 + * that the returned intent will be started. This intent is returned to 1.481 + * the client solely to let additional customization before the start. 1.482 + * </p> 1.483 + * 1.484 + * @return An {@link Intent} for launching the activity or null if the 1.485 + * policy has consumed the intent or there is not current intent 1.486 + * set via {@link #setIntent(Intent)}. 1.487 + * 1.488 + * @see HistoricalRecord 1.489 + * @see OnChooseActivityListener 1.490 + */ 1.491 + public Intent chooseActivity(int index) { 1.492 + synchronized (mInstanceLock) { 1.493 + if (mIntent == null) { 1.494 + return null; 1.495 + } 1.496 + 1.497 + ensureConsistentState(); 1.498 + 1.499 + ActivityResolveInfo chosenActivity = mActivities.get(index); 1.500 + 1.501 + ComponentName chosenName = new ComponentName( 1.502 + chosenActivity.resolveInfo.activityInfo.packageName, 1.503 + chosenActivity.resolveInfo.activityInfo.name); 1.504 + 1.505 + Intent choiceIntent = new Intent(mIntent); 1.506 + choiceIntent.setComponent(chosenName); 1.507 + 1.508 + if (mActivityChoserModelPolicy != null) { 1.509 + // Do not allow the policy to change the intent. 1.510 + Intent choiceIntentCopy = new Intent(choiceIntent); 1.511 + final boolean handled = mActivityChoserModelPolicy.onChooseActivity(this, 1.512 + choiceIntentCopy); 1.513 + if (handled) { 1.514 + return null; 1.515 + } 1.516 + } 1.517 + 1.518 + HistoricalRecord historicalRecord = new HistoricalRecord(chosenName, 1.519 + System.currentTimeMillis(), DEFAULT_HISTORICAL_RECORD_WEIGHT); 1.520 + addHistoricalRecord(historicalRecord); 1.521 + 1.522 + return choiceIntent; 1.523 + } 1.524 + } 1.525 + 1.526 + /** 1.527 + * Sets the listener for choosing an activity. 1.528 + * 1.529 + * @param listener The listener. 1.530 + */ 1.531 + public void setOnChooseActivityListener(OnChooseActivityListener listener) { 1.532 + synchronized (mInstanceLock) { 1.533 + mActivityChoserModelPolicy = listener; 1.534 + } 1.535 + } 1.536 + 1.537 + /** 1.538 + * Gets the default activity, The default activity is defined as the one 1.539 + * with highest rank i.e. the first one in the list of activities that can 1.540 + * handle the intent. 1.541 + * 1.542 + * @return The default activity, <code>null</code> id not activities. 1.543 + * 1.544 + * @see #getActivity(int) 1.545 + */ 1.546 + public ResolveInfo getDefaultActivity() { 1.547 + synchronized (mInstanceLock) { 1.548 + ensureConsistentState(); 1.549 + if (!mActivities.isEmpty()) { 1.550 + return mActivities.get(0).resolveInfo; 1.551 + } 1.552 + } 1.553 + return null; 1.554 + } 1.555 + 1.556 + /** 1.557 + * Sets the default activity. The default activity is set by adding a 1.558 + * historical record with weight high enough that this activity will 1.559 + * become the highest ranked. Such a strategy guarantees that the default 1.560 + * will eventually change if not used. Also the weight of the record for 1.561 + * setting a default is inflated with a constant amount to guarantee that 1.562 + * it will stay as default for awhile. 1.563 + * 1.564 + * @param index The index of the activity to set as default. 1.565 + */ 1.566 + public void setDefaultActivity(int index) { 1.567 + synchronized (mInstanceLock) { 1.568 + ensureConsistentState(); 1.569 + 1.570 + ActivityResolveInfo newDefaultActivity = mActivities.get(index); 1.571 + ActivityResolveInfo oldDefaultActivity = mActivities.get(0); 1.572 + 1.573 + final float weight; 1.574 + if (oldDefaultActivity != null) { 1.575 + // Add a record with weight enough to boost the chosen at the top. 1.576 + weight = oldDefaultActivity.weight - newDefaultActivity.weight 1.577 + + DEFAULT_ACTIVITY_INFLATION; 1.578 + } else { 1.579 + weight = DEFAULT_HISTORICAL_RECORD_WEIGHT; 1.580 + } 1.581 + 1.582 + ComponentName defaultName = new ComponentName( 1.583 + newDefaultActivity.resolveInfo.activityInfo.packageName, 1.584 + newDefaultActivity.resolveInfo.activityInfo.name); 1.585 + HistoricalRecord historicalRecord = new HistoricalRecord(defaultName, 1.586 + System.currentTimeMillis(), weight); 1.587 + addHistoricalRecord(historicalRecord); 1.588 + } 1.589 + } 1.590 + 1.591 + /** 1.592 + * Persists the history data to the backing file if the latter 1.593 + * was provided. Calling this method before a call to {@link #readHistoricalDataIfNeeded()} 1.594 + * throws an exception. Calling this method more than one without choosing an 1.595 + * activity has not effect. 1.596 + * 1.597 + * @throws IllegalStateException If this method is called before a call to 1.598 + * {@link #readHistoricalDataIfNeeded()}. 1.599 + */ 1.600 + private void persistHistoricalDataIfNeeded() { 1.601 + if (!mReadShareHistoryCalled) { 1.602 + throw new IllegalStateException("No preceding call to #readHistoricalData"); 1.603 + } 1.604 + if (!mHistoricalRecordsChanged) { 1.605 + return; 1.606 + } 1.607 + mHistoricalRecordsChanged = false; 1.608 + if (!TextUtils.isEmpty(mHistoryFileName)) { 1.609 + /** 1.610 + * Mozilla: Converted to a normal task.execute call so that this works on < ICS phones. 1.611 + */ 1.612 + new PersistHistoryAsyncTask().execute(new ArrayList<HistoricalRecord>(mHistoricalRecords), mHistoryFileName); 1.613 + } 1.614 + } 1.615 + 1.616 + /** 1.617 + * Sets the sorter for ordering activities based on historical data and an intent. 1.618 + * 1.619 + * @param activitySorter The sorter. 1.620 + * 1.621 + * @see ActivitySorter 1.622 + */ 1.623 + public void setActivitySorter(ActivitySorter activitySorter) { 1.624 + synchronized (mInstanceLock) { 1.625 + if (mActivitySorter == activitySorter) { 1.626 + return; 1.627 + } 1.628 + mActivitySorter = activitySorter; 1.629 + if (sortActivitiesIfNeeded()) { 1.630 + notifyChanged(); 1.631 + } 1.632 + } 1.633 + } 1.634 + 1.635 + /** 1.636 + * Sets the maximal size of the historical data. Defaults to 1.637 + * {@link #DEFAULT_HISTORY_MAX_LENGTH} 1.638 + * <p> 1.639 + * <strong>Note:</strong> Setting this property will immediately 1.640 + * enforce the specified max history size by dropping enough old 1.641 + * historical records to enforce the desired size. Thus, any 1.642 + * records that exceed the history size will be discarded and 1.643 + * irreversibly lost. 1.644 + * </p> 1.645 + * 1.646 + * @param historyMaxSize The max history size. 1.647 + */ 1.648 + public void setHistoryMaxSize(int historyMaxSize) { 1.649 + synchronized (mInstanceLock) { 1.650 + if (mHistoryMaxSize == historyMaxSize) { 1.651 + return; 1.652 + } 1.653 + mHistoryMaxSize = historyMaxSize; 1.654 + pruneExcessiveHistoricalRecordsIfNeeded(); 1.655 + if (sortActivitiesIfNeeded()) { 1.656 + notifyChanged(); 1.657 + } 1.658 + } 1.659 + } 1.660 + 1.661 + /** 1.662 + * Gets the history max size. 1.663 + * 1.664 + * @return The history max size. 1.665 + */ 1.666 + public int getHistoryMaxSize() { 1.667 + synchronized (mInstanceLock) { 1.668 + return mHistoryMaxSize; 1.669 + } 1.670 + } 1.671 + 1.672 + /** 1.673 + * Gets the history size. 1.674 + * 1.675 + * @return The history size. 1.676 + */ 1.677 + public int getHistorySize() { 1.678 + synchronized (mInstanceLock) { 1.679 + ensureConsistentState(); 1.680 + return mHistoricalRecords.size(); 1.681 + } 1.682 + } 1.683 + 1.684 + public int getDistinctActivityCountInHistory() { 1.685 + synchronized (mInstanceLock) { 1.686 + ensureConsistentState(); 1.687 + final List<String> packages = new ArrayList<String>(); 1.688 + for (HistoricalRecord record : mHistoricalRecords) { 1.689 + String activity = record.activity.flattenToString(); 1.690 + if (!packages.contains(activity)) { 1.691 + packages.add(activity); 1.692 + } 1.693 + } 1.694 + return packages.size(); 1.695 + } 1.696 + } 1.697 + 1.698 + @Override 1.699 + protected void finalize() throws Throwable { 1.700 + super.finalize(); 1.701 + 1.702 + /** 1.703 + * Mozilla: Not needed for the application. 1.704 + */ 1.705 + mPackageMonitor.unregister(); 1.706 + } 1.707 + 1.708 + /** 1.709 + * Ensures the model is in a consistent state which is the 1.710 + * activities for the current intent have been loaded, the 1.711 + * most recent history has been read, and the activities 1.712 + * are sorted. 1.713 + */ 1.714 + private void ensureConsistentState() { 1.715 + boolean stateChanged = loadActivitiesIfNeeded(); 1.716 + stateChanged |= readHistoricalDataIfNeeded(); 1.717 + pruneExcessiveHistoricalRecordsIfNeeded(); 1.718 + if (stateChanged) { 1.719 + sortActivitiesIfNeeded(); 1.720 + notifyChanged(); 1.721 + } 1.722 + } 1.723 + 1.724 + /** 1.725 + * Sorts the activities if necessary which is if there is a 1.726 + * sorter, there are some activities to sort, and there is some 1.727 + * historical data. 1.728 + * 1.729 + * @return Whether sorting was performed. 1.730 + */ 1.731 + private boolean sortActivitiesIfNeeded() { 1.732 + if (mActivitySorter != null && mIntent != null 1.733 + && !mActivities.isEmpty() && !mHistoricalRecords.isEmpty()) { 1.734 + mActivitySorter.sort(mIntent, mActivities, 1.735 + Collections.unmodifiableList(mHistoricalRecords)); 1.736 + return true; 1.737 + } 1.738 + return false; 1.739 + } 1.740 + 1.741 + /** 1.742 + * Loads the activities for the current intent if needed which is 1.743 + * if they are not already loaded for the current intent. 1.744 + * 1.745 + * @return Whether loading was performed. 1.746 + */ 1.747 + private boolean loadActivitiesIfNeeded() { 1.748 + if (mReloadActivities && mIntent != null) { 1.749 + mReloadActivities = false; 1.750 + mActivities.clear(); 1.751 + List<ResolveInfo> resolveInfos = mContext.getPackageManager() 1.752 + .queryIntentActivities(mIntent, 0); 1.753 + final int resolveInfoCount = resolveInfos.size(); 1.754 + for (int i = 0; i < resolveInfoCount; i++) { 1.755 + ResolveInfo resolveInfo = resolveInfos.get(i); 1.756 + mActivities.add(new ActivityResolveInfo(resolveInfo)); 1.757 + } 1.758 + return true; 1.759 + } 1.760 + return false; 1.761 + } 1.762 + 1.763 + /** 1.764 + * Reads the historical data if necessary which is it has 1.765 + * changed, there is a history file, and there is not persist 1.766 + * in progress. 1.767 + * 1.768 + * @return Whether reading was performed. 1.769 + */ 1.770 + private boolean readHistoricalDataIfNeeded() { 1.771 + if (mCanReadHistoricalData && mHistoricalRecordsChanged && 1.772 + !TextUtils.isEmpty(mHistoryFileName)) { 1.773 + mCanReadHistoricalData = false; 1.774 + mReadShareHistoryCalled = true; 1.775 + readHistoricalDataImpl(); 1.776 + return true; 1.777 + } 1.778 + return false; 1.779 + } 1.780 + 1.781 + /** 1.782 + * Adds a historical record. 1.783 + * 1.784 + * @param historicalRecord The record to add. 1.785 + * @return True if the record was added. 1.786 + */ 1.787 + private boolean addHistoricalRecord(HistoricalRecord historicalRecord) { 1.788 + final boolean added = mHistoricalRecords.add(historicalRecord); 1.789 + if (added) { 1.790 + mHistoricalRecordsChanged = true; 1.791 + pruneExcessiveHistoricalRecordsIfNeeded(); 1.792 + persistHistoricalDataIfNeeded(); 1.793 + sortActivitiesIfNeeded(); 1.794 + notifyChanged(); 1.795 + } 1.796 + return added; 1.797 + } 1.798 + 1.799 + /** 1.800 + * Removes all historical records for this pkg. 1.801 + * 1.802 + * @param historicalRecord The pkg to delete records for. 1.803 + * @return True if the record was added. 1.804 + */ 1.805 + private boolean removeHistoricalRecordsForPackage(final String pkg) { 1.806 + boolean removed = false; 1.807 + 1.808 + for (Iterator<HistoricalRecord> i = mHistoricalRecords.iterator(); i.hasNext();) { 1.809 + final HistoricalRecord record = i.next(); 1.810 + if (record.activity.getPackageName().equals(pkg)) { 1.811 + i.remove(); 1.812 + removed = true; 1.813 + } 1.814 + } 1.815 + 1.816 + if (removed) { 1.817 + mHistoricalRecordsChanged = true; 1.818 + pruneExcessiveHistoricalRecordsIfNeeded(); 1.819 + persistHistoricalDataIfNeeded(); 1.820 + sortActivitiesIfNeeded(); 1.821 + notifyChanged(); 1.822 + } 1.823 + 1.824 + return removed; 1.825 + } 1.826 + 1.827 + /** 1.828 + * Prunes older excessive records to guarantee maxHistorySize. 1.829 + */ 1.830 + private void pruneExcessiveHistoricalRecordsIfNeeded() { 1.831 + final int pruneCount = mHistoricalRecords.size() - mHistoryMaxSize; 1.832 + if (pruneCount <= 0) { 1.833 + return; 1.834 + } 1.835 + mHistoricalRecordsChanged = true; 1.836 + for (int i = 0; i < pruneCount; i++) { 1.837 + HistoricalRecord prunedRecord = mHistoricalRecords.remove(0); 1.838 + if (DEBUG) { 1.839 + Log.i(LOG_TAG, "Pruned: " + prunedRecord); 1.840 + } 1.841 + } 1.842 + } 1.843 + 1.844 + /** 1.845 + * Represents a record in the history. 1.846 + */ 1.847 + public final static class HistoricalRecord { 1.848 + 1.849 + /** 1.850 + * The activity name. 1.851 + */ 1.852 + public final ComponentName activity; 1.853 + 1.854 + /** 1.855 + * The choice time. 1.856 + */ 1.857 + public final long time; 1.858 + 1.859 + /** 1.860 + * The record weight. 1.861 + */ 1.862 + public final float weight; 1.863 + 1.864 + /** 1.865 + * Creates a new instance. 1.866 + * 1.867 + * @param activityName The activity component name flattened to string. 1.868 + * @param time The time the activity was chosen. 1.869 + * @param weight The weight of the record. 1.870 + */ 1.871 + public HistoricalRecord(String activityName, long time, float weight) { 1.872 + this(ComponentName.unflattenFromString(activityName), time, weight); 1.873 + } 1.874 + 1.875 + /** 1.876 + * Creates a new instance. 1.877 + * 1.878 + * @param activityName The activity name. 1.879 + * @param time The time the activity was chosen. 1.880 + * @param weight The weight of the record. 1.881 + */ 1.882 + public HistoricalRecord(ComponentName activityName, long time, float weight) { 1.883 + this.activity = activityName; 1.884 + this.time = time; 1.885 + this.weight = weight; 1.886 + } 1.887 + 1.888 + @Override 1.889 + public int hashCode() { 1.890 + final int prime = 31; 1.891 + int result = 1; 1.892 + result = prime * result + ((activity == null) ? 0 : activity.hashCode()); 1.893 + result = prime * result + (int) (time ^ (time >>> 32)); 1.894 + result = prime * result + Float.floatToIntBits(weight); 1.895 + return result; 1.896 + } 1.897 + 1.898 + @Override 1.899 + public boolean equals(Object obj) { 1.900 + if (this == obj) { 1.901 + return true; 1.902 + } 1.903 + if (obj == null) { 1.904 + return false; 1.905 + } 1.906 + if (getClass() != obj.getClass()) { 1.907 + return false; 1.908 + } 1.909 + HistoricalRecord other = (HistoricalRecord) obj; 1.910 + if (activity == null) { 1.911 + if (other.activity != null) { 1.912 + return false; 1.913 + } 1.914 + } else if (!activity.equals(other.activity)) { 1.915 + return false; 1.916 + } 1.917 + if (time != other.time) { 1.918 + return false; 1.919 + } 1.920 + if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) { 1.921 + return false; 1.922 + } 1.923 + return true; 1.924 + } 1.925 + 1.926 + @Override 1.927 + public String toString() { 1.928 + StringBuilder builder = new StringBuilder(); 1.929 + builder.append("["); 1.930 + builder.append("; activity:").append(activity); 1.931 + builder.append("; time:").append(time); 1.932 + builder.append("; weight:").append(new BigDecimal(weight)); 1.933 + builder.append("]"); 1.934 + return builder.toString(); 1.935 + } 1.936 + } 1.937 + 1.938 + /** 1.939 + * Represents an activity. 1.940 + */ 1.941 + public final class ActivityResolveInfo implements Comparable<ActivityResolveInfo> { 1.942 + 1.943 + /** 1.944 + * The {@link ResolveInfo} of the activity. 1.945 + */ 1.946 + public final ResolveInfo resolveInfo; 1.947 + 1.948 + /** 1.949 + * Weight of the activity. Useful for sorting. 1.950 + */ 1.951 + public float weight; 1.952 + 1.953 + /** 1.954 + * Creates a new instance. 1.955 + * 1.956 + * @param resolveInfo activity {@link ResolveInfo}. 1.957 + */ 1.958 + public ActivityResolveInfo(ResolveInfo resolveInfo) { 1.959 + this.resolveInfo = resolveInfo; 1.960 + } 1.961 + 1.962 + @Override 1.963 + public int hashCode() { 1.964 + return 31 + Float.floatToIntBits(weight); 1.965 + } 1.966 + 1.967 + @Override 1.968 + public boolean equals(Object obj) { 1.969 + if (this == obj) { 1.970 + return true; 1.971 + } 1.972 + if (obj == null) { 1.973 + return false; 1.974 + } 1.975 + if (getClass() != obj.getClass()) { 1.976 + return false; 1.977 + } 1.978 + ActivityResolveInfo other = (ActivityResolveInfo) obj; 1.979 + if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) { 1.980 + return false; 1.981 + } 1.982 + return true; 1.983 + } 1.984 + 1.985 + public int compareTo(ActivityResolveInfo another) { 1.986 + return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight); 1.987 + } 1.988 + 1.989 + @Override 1.990 + public String toString() { 1.991 + StringBuilder builder = new StringBuilder(); 1.992 + builder.append("["); 1.993 + builder.append("resolveInfo:").append(resolveInfo.toString()); 1.994 + builder.append("; weight:").append(new BigDecimal(weight)); 1.995 + builder.append("]"); 1.996 + return builder.toString(); 1.997 + } 1.998 + } 1.999 + 1.1000 + /** 1.1001 + * Default activity sorter implementation. 1.1002 + */ 1.1003 + private final class DefaultSorter implements ActivitySorter { 1.1004 + private static final float WEIGHT_DECAY_COEFFICIENT = 0.95f; 1.1005 + 1.1006 + private final Map<String, ActivityResolveInfo> mPackageNameToActivityMap = 1.1007 + new HashMap<String, ActivityResolveInfo>(); 1.1008 + 1.1009 + public void sort(Intent intent, List<ActivityResolveInfo> activities, 1.1010 + List<HistoricalRecord> historicalRecords) { 1.1011 + Map<String, ActivityResolveInfo> packageNameToActivityMap = 1.1012 + mPackageNameToActivityMap; 1.1013 + packageNameToActivityMap.clear(); 1.1014 + 1.1015 + final int activityCount = activities.size(); 1.1016 + for (int i = 0; i < activityCount; i++) { 1.1017 + ActivityResolveInfo activity = activities.get(i); 1.1018 + activity.weight = 0.0f; 1.1019 + 1.1020 + // Make sure we're using a non-ambiguous name here 1.1021 + ComponentName chosenName = new ComponentName( 1.1022 + activity.resolveInfo.activityInfo.packageName, 1.1023 + activity.resolveInfo.activityInfo.name); 1.1024 + String packageName = chosenName.flattenToString(); 1.1025 + packageNameToActivityMap.put(packageName, activity); 1.1026 + } 1.1027 + 1.1028 + final int lastShareIndex = historicalRecords.size() - 1; 1.1029 + float nextRecordWeight = 1; 1.1030 + for (int i = lastShareIndex; i >= 0; i--) { 1.1031 + HistoricalRecord historicalRecord = historicalRecords.get(i); 1.1032 + String packageName = historicalRecord.activity.flattenToString(); 1.1033 + ActivityResolveInfo activity = packageNameToActivityMap.get(packageName); 1.1034 + if (activity != null) { 1.1035 + activity.weight += historicalRecord.weight * nextRecordWeight; 1.1036 + nextRecordWeight = nextRecordWeight * WEIGHT_DECAY_COEFFICIENT; 1.1037 + } 1.1038 + } 1.1039 + 1.1040 + Collections.sort(activities); 1.1041 + 1.1042 + if (DEBUG) { 1.1043 + for (int i = 0; i < activityCount; i++) { 1.1044 + Log.i(LOG_TAG, "Sorted: " + activities.get(i)); 1.1045 + } 1.1046 + } 1.1047 + } 1.1048 + } 1.1049 + 1.1050 + /** 1.1051 + * Command for reading the historical records from a file off the UI thread. 1.1052 + */ 1.1053 + private void readHistoricalDataImpl() { 1.1054 + FileInputStream fis = null; 1.1055 + try { 1.1056 + GeckoProfile profile = GeckoProfile.get(mContext); 1.1057 + File f = profile.getFile(mHistoryFileName); 1.1058 + if (!f.exists()) { 1.1059 + // Fall back to the non-profile aware file if it exists... 1.1060 + File oldFile = new File(mHistoryFileName); 1.1061 + oldFile.renameTo(f); 1.1062 + } 1.1063 + fis = new FileInputStream(f); 1.1064 + } catch (FileNotFoundException fnfe) { 1.1065 + try { 1.1066 + Distribution dist = new Distribution(mContext); 1.1067 + File distFile = dist.getDistributionFile("quickshare/" + mHistoryFileName); 1.1068 + if (distFile == null) { 1.1069 + if (DEBUG) { 1.1070 + Log.i(LOG_TAG, "Could not open historical records file: " + mHistoryFileName); 1.1071 + } 1.1072 + return; 1.1073 + } 1.1074 + fis = new FileInputStream(distFile); 1.1075 + } catch(Exception ex) { 1.1076 + if (DEBUG) { 1.1077 + Log.i(LOG_TAG, "Could not open historical records file: " + mHistoryFileName); 1.1078 + } 1.1079 + return; 1.1080 + } 1.1081 + } 1.1082 + 1.1083 + try { 1.1084 + XmlPullParser parser = Xml.newPullParser(); 1.1085 + parser.setInput(fis, null); 1.1086 + 1.1087 + int type = XmlPullParser.START_DOCUMENT; 1.1088 + while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) { 1.1089 + type = parser.next(); 1.1090 + } 1.1091 + 1.1092 + if (!TAG_HISTORICAL_RECORDS.equals(parser.getName())) { 1.1093 + throw new XmlPullParserException("Share records file does not start with " 1.1094 + + TAG_HISTORICAL_RECORDS + " tag."); 1.1095 + } 1.1096 + 1.1097 + List<HistoricalRecord> historicalRecords = mHistoricalRecords; 1.1098 + historicalRecords.clear(); 1.1099 + 1.1100 + while (true) { 1.1101 + type = parser.next(); 1.1102 + if (type == XmlPullParser.END_DOCUMENT) { 1.1103 + break; 1.1104 + } 1.1105 + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 1.1106 + continue; 1.1107 + } 1.1108 + String nodeName = parser.getName(); 1.1109 + if (!TAG_HISTORICAL_RECORD.equals(nodeName)) { 1.1110 + throw new XmlPullParserException("Share records file not well-formed."); 1.1111 + } 1.1112 + 1.1113 + String activity = parser.getAttributeValue(null, ATTRIBUTE_ACTIVITY); 1.1114 + final long time = 1.1115 + Long.parseLong(parser.getAttributeValue(null, ATTRIBUTE_TIME)); 1.1116 + final float weight = 1.1117 + Float.parseFloat(parser.getAttributeValue(null, ATTRIBUTE_WEIGHT)); 1.1118 + HistoricalRecord readRecord = new HistoricalRecord(activity, time, weight); 1.1119 + historicalRecords.add(readRecord); 1.1120 + 1.1121 + if (DEBUG) { 1.1122 + Log.i(LOG_TAG, "Read " + readRecord.toString()); 1.1123 + } 1.1124 + } 1.1125 + 1.1126 + if (DEBUG) { 1.1127 + Log.i(LOG_TAG, "Read " + historicalRecords.size() + " historical records."); 1.1128 + } 1.1129 + } catch (XmlPullParserException xppe) { 1.1130 + Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, xppe); 1.1131 + } catch (IOException ioe) { 1.1132 + Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, ioe); 1.1133 + } finally { 1.1134 + if (fis != null) { 1.1135 + try { 1.1136 + fis.close(); 1.1137 + } catch (IOException ioe) { 1.1138 + /* ignore */ 1.1139 + } 1.1140 + } 1.1141 + } 1.1142 + } 1.1143 + 1.1144 + /** 1.1145 + * Command for persisting the historical records to a file off the UI thread. 1.1146 + */ 1.1147 + private final class PersistHistoryAsyncTask extends AsyncTask<Object, Void, Void> { 1.1148 + 1.1149 + @Override 1.1150 + @SuppressWarnings("unchecked") 1.1151 + public Void doInBackground(Object... args) { 1.1152 + List<HistoricalRecord> historicalRecords = (List<HistoricalRecord>) args[0]; 1.1153 + String historyFileName = (String) args[1]; 1.1154 + 1.1155 + FileOutputStream fos = null; 1.1156 + 1.1157 + try { 1.1158 + // Mozilla - Update the location we save files to 1.1159 + GeckoProfile profile = GeckoProfile.get(mContext); 1.1160 + File file = profile.getFile(historyFileName); 1.1161 + fos = new FileOutputStream(file); 1.1162 + } catch (FileNotFoundException fnfe) { 1.1163 + Log.e(LOG_TAG, "Error writing historical record file: " + historyFileName, fnfe); 1.1164 + return null; 1.1165 + } 1.1166 + 1.1167 + XmlSerializer serializer = Xml.newSerializer(); 1.1168 + 1.1169 + try { 1.1170 + serializer.setOutput(fos, null); 1.1171 + serializer.startDocument("UTF-8", true); 1.1172 + serializer.startTag(null, TAG_HISTORICAL_RECORDS); 1.1173 + 1.1174 + final int recordCount = historicalRecords.size(); 1.1175 + for (int i = 0; i < recordCount; i++) { 1.1176 + HistoricalRecord record = historicalRecords.remove(0); 1.1177 + serializer.startTag(null, TAG_HISTORICAL_RECORD); 1.1178 + serializer.attribute(null, ATTRIBUTE_ACTIVITY, 1.1179 + record.activity.flattenToString()); 1.1180 + serializer.attribute(null, ATTRIBUTE_TIME, String.valueOf(record.time)); 1.1181 + serializer.attribute(null, ATTRIBUTE_WEIGHT, String.valueOf(record.weight)); 1.1182 + serializer.endTag(null, TAG_HISTORICAL_RECORD); 1.1183 + if (DEBUG) { 1.1184 + Log.i(LOG_TAG, "Wrote " + record.toString()); 1.1185 + } 1.1186 + } 1.1187 + 1.1188 + serializer.endTag(null, TAG_HISTORICAL_RECORDS); 1.1189 + serializer.endDocument(); 1.1190 + 1.1191 + if (DEBUG) { 1.1192 + Log.i(LOG_TAG, "Wrote " + recordCount + " historical records."); 1.1193 + } 1.1194 + } catch (IllegalArgumentException iae) { 1.1195 + Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, iae); 1.1196 + } catch (IllegalStateException ise) { 1.1197 + Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ise); 1.1198 + } catch (IOException ioe) { 1.1199 + Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ioe); 1.1200 + } finally { 1.1201 + mCanReadHistoricalData = true; 1.1202 + if (fos != null) { 1.1203 + try { 1.1204 + fos.close(); 1.1205 + } catch (IOException e) { 1.1206 + /* ignore */ 1.1207 + } 1.1208 + } 1.1209 + } 1.1210 + return null; 1.1211 + } 1.1212 + } 1.1213 + 1.1214 + /** 1.1215 + * Keeps in sync the historical records and activities with the installed applications. 1.1216 + */ 1.1217 + /** 1.1218 + * Mozilla: Adapted significantly 1.1219 + */ 1.1220 + private static final String LOGTAG = "GeckoActivityChooserModel"; 1.1221 + private final class DataModelPackageMonitor extends BroadcastReceiver { 1.1222 + private Context mContext; 1.1223 + 1.1224 + public DataModelPackageMonitor() { } 1.1225 + 1.1226 + public void register(Context context) { 1.1227 + mContext = context; 1.1228 + 1.1229 + String[] intents = new String[] { 1.1230 + Intent.ACTION_PACKAGE_REMOVED, 1.1231 + Intent.ACTION_PACKAGE_ADDED, 1.1232 + Intent.ACTION_PACKAGE_CHANGED 1.1233 + }; 1.1234 + 1.1235 + for (String intent : intents) { 1.1236 + IntentFilter removeFilter = new IntentFilter(intent); 1.1237 + removeFilter.addDataScheme("package"); 1.1238 + context.registerReceiver(this, removeFilter); 1.1239 + } 1.1240 + } 1.1241 + 1.1242 + public void unregister() { 1.1243 + mContext.unregisterReceiver(this); 1.1244 + mContext = null; 1.1245 + } 1.1246 + 1.1247 + @Override 1.1248 + public void onReceive(Context context, Intent intent) { 1.1249 + String action = intent.getAction(); 1.1250 + if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { 1.1251 + String packageName = intent.getData().getSchemeSpecificPart(); 1.1252 + removeHistoricalRecordsForPackage(packageName); 1.1253 + } 1.1254 + 1.1255 + mReloadActivities = true; 1.1256 + } 1.1257 + } 1.1258 +} 1.1259 +