mobile/android/base/widget/ActivityChooserModel.java

changeset 0
6474c204b198
     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 +

mercurial