mobile/android/base/widget/ActivityChooserModel.java

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /*
michael@0 2 * Copyright (C) 2011 The Android Open Source Project
michael@0 3 *
michael@0 4 * Licensed under the Apache License, Version 2.0 (the "License");
michael@0 5 * you may not use this file except in compliance with the License.
michael@0 6 * You may obtain a copy of the License at
michael@0 7 *
michael@0 8 * http://www.apache.org/licenses/LICENSE-2.0
michael@0 9 *
michael@0 10 * Unless required by applicable law or agreed to in writing, software
michael@0 11 * distributed under the License is distributed on an "AS IS" BASIS,
michael@0 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
michael@0 13 * See the License for the specific language governing permissions and
michael@0 14 * limitations under the License.
michael@0 15 */
michael@0 16
michael@0 17 /**
michael@0 18 * Mozilla: Changing the package.
michael@0 19 */
michael@0 20 //package android.widget;
michael@0 21 package org.mozilla.gecko.widget;
michael@0 22
michael@0 23 // Mozilla: New import
michael@0 24 import org.mozilla.gecko.Distribution;
michael@0 25 import org.mozilla.gecko.GeckoProfile;
michael@0 26 import java.io.File;
michael@0 27
michael@0 28 import android.content.BroadcastReceiver;
michael@0 29 import android.content.ComponentName;
michael@0 30 import android.content.Context;
michael@0 31 import android.content.Intent;
michael@0 32 import android.content.IntentFilter;
michael@0 33 import android.content.pm.ResolveInfo;
michael@0 34 import android.database.DataSetObservable;
michael@0 35 import android.os.AsyncTask;
michael@0 36 import android.text.TextUtils;
michael@0 37 import android.util.Log;
michael@0 38 import android.util.Xml;
michael@0 39
michael@0 40 /**
michael@0 41 * Mozilla: Unused import.
michael@0 42 */
michael@0 43 //import com.android.internal.content.PackageMonitor;
michael@0 44
michael@0 45 import org.xmlpull.v1.XmlPullParser;
michael@0 46 import org.xmlpull.v1.XmlPullParserException;
michael@0 47 import org.xmlpull.v1.XmlSerializer;
michael@0 48
michael@0 49 import java.io.FileInputStream;
michael@0 50 import java.io.FileNotFoundException;
michael@0 51 import java.io.FileOutputStream;
michael@0 52 import java.io.IOException;
michael@0 53 import java.math.BigDecimal;
michael@0 54 import java.util.ArrayList;
michael@0 55 import java.util.Collections;
michael@0 56 import java.util.HashMap;
michael@0 57 import java.util.Iterator;
michael@0 58 import java.util.List;
michael@0 59 import java.util.Map;
michael@0 60
michael@0 61 /**
michael@0 62 * <p>
michael@0 63 * This class represents a data model for choosing a component for handing a
michael@0 64 * given {@link Intent}. The model is responsible for querying the system for
michael@0 65 * activities that can handle the given intent and order found activities
michael@0 66 * based on historical data of previous choices. The historical data is stored
michael@0 67 * in an application private file. If a client does not want to have persistent
michael@0 68 * choice history the file can be omitted, thus the activities will be ordered
michael@0 69 * based on historical usage for the current session.
michael@0 70 * <p>
michael@0 71 * </p>
michael@0 72 * For each backing history file there is a singleton instance of this class. Thus,
michael@0 73 * several clients that specify the same history file will share the same model. Note
michael@0 74 * that if multiple clients are sharing the same model they should implement semantically
michael@0 75 * equivalent functionality since setting the model intent will change the found
michael@0 76 * activities and they may be inconsistent with the functionality of some of the clients.
michael@0 77 * For example, choosing a share activity can be implemented by a single backing
michael@0 78 * model and two different views for performing the selection. If however, one of the
michael@0 79 * views is used for sharing but the other for importing, for example, then each
michael@0 80 * view should be backed by a separate model.
michael@0 81 * </p>
michael@0 82 * <p>
michael@0 83 * The way clients interact with this class is as follows:
michael@0 84 * </p>
michael@0 85 * <p>
michael@0 86 * <pre>
michael@0 87 * <code>
michael@0 88 * // Get a model and set it to a couple of clients with semantically similar function.
michael@0 89 * ActivityChooserModel dataModel =
michael@0 90 * ActivityChooserModel.get(context, "task_specific_history_file_name.xml");
michael@0 91 *
michael@0 92 * ActivityChooserModelClient modelClient1 = getActivityChooserModelClient1();
michael@0 93 * modelClient1.setActivityChooserModel(dataModel);
michael@0 94 *
michael@0 95 * ActivityChooserModelClient modelClient2 = getActivityChooserModelClient2();
michael@0 96 * modelClient2.setActivityChooserModel(dataModel);
michael@0 97 *
michael@0 98 * // Set an intent to choose a an activity for.
michael@0 99 * dataModel.setIntent(intent);
michael@0 100 * <pre>
michael@0 101 * <code>
michael@0 102 * </p>
michael@0 103 * <p>
michael@0 104 * <strong>Note:</strong> This class is thread safe.
michael@0 105 * </p>
michael@0 106 *
michael@0 107 * @hide
michael@0 108 */
michael@0 109 public class ActivityChooserModel extends DataSetObservable {
michael@0 110
michael@0 111 /**
michael@0 112 * Client that utilizes an {@link ActivityChooserModel}.
michael@0 113 */
michael@0 114 public interface ActivityChooserModelClient {
michael@0 115
michael@0 116 /**
michael@0 117 * Sets the {@link ActivityChooserModel}.
michael@0 118 *
michael@0 119 * @param dataModel The model.
michael@0 120 */
michael@0 121 public void setActivityChooserModel(ActivityChooserModel dataModel);
michael@0 122 }
michael@0 123
michael@0 124 /**
michael@0 125 * Defines a sorter that is responsible for sorting the activities
michael@0 126 * based on the provided historical choices and an intent.
michael@0 127 */
michael@0 128 public interface ActivitySorter {
michael@0 129
michael@0 130 /**
michael@0 131 * Sorts the <code>activities</code> in descending order of relevance
michael@0 132 * based on previous history and an intent.
michael@0 133 *
michael@0 134 * @param intent The {@link Intent}.
michael@0 135 * @param activities Activities to be sorted.
michael@0 136 * @param historicalRecords Historical records.
michael@0 137 */
michael@0 138 // This cannot be done by a simple comparator since an Activity weight
michael@0 139 // is computed from history. Note that Activity implements Comparable.
michael@0 140 public void sort(Intent intent, List<ActivityResolveInfo> activities,
michael@0 141 List<HistoricalRecord> historicalRecords);
michael@0 142 }
michael@0 143
michael@0 144 /**
michael@0 145 * Listener for choosing an activity.
michael@0 146 */
michael@0 147 public interface OnChooseActivityListener {
michael@0 148
michael@0 149 /**
michael@0 150 * Called when an activity has been chosen. The client can decide whether
michael@0 151 * an activity can be chosen and if so the caller of
michael@0 152 * {@link ActivityChooserModel#chooseActivity(int)} will receive and {@link Intent}
michael@0 153 * for launching it.
michael@0 154 * <p>
michael@0 155 * <strong>Note:</strong> Modifying the intent is not permitted and
michael@0 156 * any changes to the latter will be ignored.
michael@0 157 * </p>
michael@0 158 *
michael@0 159 * @param host The listener's host model.
michael@0 160 * @param intent The intent for launching the chosen activity.
michael@0 161 * @return Whether the intent is handled and should not be delivered to clients.
michael@0 162 *
michael@0 163 * @see ActivityChooserModel#chooseActivity(int)
michael@0 164 */
michael@0 165 public boolean onChooseActivity(ActivityChooserModel host, Intent intent);
michael@0 166 }
michael@0 167
michael@0 168 /**
michael@0 169 * Flag for selecting debug mode.
michael@0 170 */
michael@0 171 private static final boolean DEBUG = false;
michael@0 172
michael@0 173 /**
michael@0 174 * Tag used for logging.
michael@0 175 */
michael@0 176 private static final String LOG_TAG = ActivityChooserModel.class.getSimpleName();
michael@0 177
michael@0 178 /**
michael@0 179 * The root tag in the history file.
michael@0 180 */
michael@0 181 private static final String TAG_HISTORICAL_RECORDS = "historical-records";
michael@0 182
michael@0 183 /**
michael@0 184 * The tag for a record in the history file.
michael@0 185 */
michael@0 186 private static final String TAG_HISTORICAL_RECORD = "historical-record";
michael@0 187
michael@0 188 /**
michael@0 189 * Attribute for the activity.
michael@0 190 */
michael@0 191 private static final String ATTRIBUTE_ACTIVITY = "activity";
michael@0 192
michael@0 193 /**
michael@0 194 * Attribute for the choice time.
michael@0 195 */
michael@0 196 private static final String ATTRIBUTE_TIME = "time";
michael@0 197
michael@0 198 /**
michael@0 199 * Attribute for the choice weight.
michael@0 200 */
michael@0 201 private static final String ATTRIBUTE_WEIGHT = "weight";
michael@0 202
michael@0 203 /**
michael@0 204 * The default name of the choice history file.
michael@0 205 */
michael@0 206 public static final String DEFAULT_HISTORY_FILE_NAME =
michael@0 207 "activity_choser_model_history.xml";
michael@0 208
michael@0 209 /**
michael@0 210 * The default maximal length of the choice history.
michael@0 211 */
michael@0 212 public static final int DEFAULT_HISTORY_MAX_LENGTH = 50;
michael@0 213
michael@0 214 /**
michael@0 215 * The amount with which to inflate a chosen activity when set as default.
michael@0 216 */
michael@0 217 private static final int DEFAULT_ACTIVITY_INFLATION = 5;
michael@0 218
michael@0 219 /**
michael@0 220 * Default weight for a choice record.
michael@0 221 */
michael@0 222 private static final float DEFAULT_HISTORICAL_RECORD_WEIGHT = 1.0f;
michael@0 223
michael@0 224 /**
michael@0 225 * The extension of the history file.
michael@0 226 */
michael@0 227 private static final String HISTORY_FILE_EXTENSION = ".xml";
michael@0 228
michael@0 229 /**
michael@0 230 * An invalid item index.
michael@0 231 */
michael@0 232 private static final int INVALID_INDEX = -1;
michael@0 233
michael@0 234 /**
michael@0 235 * Lock to guard the model registry.
michael@0 236 */
michael@0 237 private static final Object sRegistryLock = new Object();
michael@0 238
michael@0 239 /**
michael@0 240 * This the registry for data models.
michael@0 241 */
michael@0 242 private static final Map<String, ActivityChooserModel> sDataModelRegistry =
michael@0 243 new HashMap<String, ActivityChooserModel>();
michael@0 244
michael@0 245 /**
michael@0 246 * Lock for synchronizing on this instance.
michael@0 247 */
michael@0 248 private final Object mInstanceLock = new Object();
michael@0 249
michael@0 250 /**
michael@0 251 * List of activities that can handle the current intent.
michael@0 252 */
michael@0 253 private final List<ActivityResolveInfo> mActivities = new ArrayList<ActivityResolveInfo>();
michael@0 254
michael@0 255 /**
michael@0 256 * List with historical choice records.
michael@0 257 */
michael@0 258 private final List<HistoricalRecord> mHistoricalRecords = new ArrayList<HistoricalRecord>();
michael@0 259
michael@0 260 /**
michael@0 261 * Monitor for added and removed packages.
michael@0 262 */
michael@0 263 /**
michael@0 264 * Mozilla: Converted from a PackageMonitor to a DataModelPackageMonitor to avoid importing a new class.
michael@0 265 */
michael@0 266 private final DataModelPackageMonitor mPackageMonitor = new DataModelPackageMonitor();
michael@0 267
michael@0 268 /**
michael@0 269 * Context for accessing resources.
michael@0 270 */
michael@0 271 private final Context mContext;
michael@0 272
michael@0 273 /**
michael@0 274 * The name of the history file that backs this model.
michael@0 275 */
michael@0 276 private final String mHistoryFileName;
michael@0 277
michael@0 278 /**
michael@0 279 * The intent for which a activity is being chosen.
michael@0 280 */
michael@0 281 private Intent mIntent;
michael@0 282
michael@0 283 /**
michael@0 284 * The sorter for ordering activities based on intent and past choices.
michael@0 285 */
michael@0 286 private ActivitySorter mActivitySorter = new DefaultSorter();
michael@0 287
michael@0 288 /**
michael@0 289 * The maximal length of the choice history.
michael@0 290 */
michael@0 291 private int mHistoryMaxSize = DEFAULT_HISTORY_MAX_LENGTH;
michael@0 292
michael@0 293 /**
michael@0 294 * Flag whether choice history can be read. In general many clients can
michael@0 295 * share the same data model and {@link #readHistoricalDataIfNeeded()} may be called
michael@0 296 * by arbitrary of them any number of times. Therefore, this class guarantees
michael@0 297 * that the very first read succeeds and subsequent reads can be performed
michael@0 298 * only after a call to {@link #persistHistoricalDataIfNeeded()} followed by change
michael@0 299 * of the share records.
michael@0 300 */
michael@0 301 private boolean mCanReadHistoricalData = true;
michael@0 302
michael@0 303 /**
michael@0 304 * Flag whether the choice history was read. This is used to enforce that
michael@0 305 * before calling {@link #persistHistoricalDataIfNeeded()} a call to
michael@0 306 * {@link #persistHistoricalDataIfNeeded()} has been made. This aims to avoid a
michael@0 307 * scenario in which a choice history file exits, it is not read yet and
michael@0 308 * it is overwritten. Note that always all historical records are read in
michael@0 309 * full and the file is rewritten. This is necessary since we need to
michael@0 310 * purge old records that are outside of the sliding window of past choices.
michael@0 311 */
michael@0 312 private boolean mReadShareHistoryCalled = false;
michael@0 313
michael@0 314 /**
michael@0 315 * Flag whether the choice records have changed. In general many clients can
michael@0 316 * share the same data model and {@link #persistHistoricalDataIfNeeded()} may be called
michael@0 317 * by arbitrary of them any number of times. Therefore, this class guarantees
michael@0 318 * that choice history will be persisted only if it has changed.
michael@0 319 */
michael@0 320 private boolean mHistoricalRecordsChanged = true;
michael@0 321
michael@0 322 /**
michael@0 323 * Flag whether to reload the activities for the current intent.
michael@0 324 */
michael@0 325 private boolean mReloadActivities = false;
michael@0 326
michael@0 327 /**
michael@0 328 * Policy for controlling how the model handles chosen activities.
michael@0 329 */
michael@0 330 private OnChooseActivityListener mActivityChoserModelPolicy;
michael@0 331
michael@0 332 /**
michael@0 333 * Gets the data model backed by the contents of the provided file with historical data.
michael@0 334 * Note that only one data model is backed by a given file, thus multiple calls with
michael@0 335 * the same file name will return the same model instance. If no such instance is present
michael@0 336 * it is created.
michael@0 337 * <p>
michael@0 338 * <strong>Note:</strong> To use the default historical data file clients should explicitly
michael@0 339 * pass as file name {@link #DEFAULT_HISTORY_FILE_NAME}. If no persistence of the choice
michael@0 340 * history is desired clients should pass <code>null</code> for the file name. In such
michael@0 341 * case a new model is returned for each invocation.
michael@0 342 * </p>
michael@0 343 *
michael@0 344 * <p>
michael@0 345 * <strong>Always use difference historical data files for semantically different actions.
michael@0 346 * For example, sharing is different from importing.</strong>
michael@0 347 * </p>
michael@0 348 *
michael@0 349 * @param context Context for loading resources.
michael@0 350 * @param historyFileName File name with choice history, <code>null</code>
michael@0 351 * if the model should not be backed by a file. In this case the activities
michael@0 352 * will be ordered only by data from the current session.
michael@0 353 *
michael@0 354 * @return The model.
michael@0 355 */
michael@0 356 public static ActivityChooserModel get(Context context, String historyFileName) {
michael@0 357 synchronized (sRegistryLock) {
michael@0 358 ActivityChooserModel dataModel = sDataModelRegistry.get(historyFileName);
michael@0 359 if (dataModel == null) {
michael@0 360 dataModel = new ActivityChooserModel(context, historyFileName);
michael@0 361 sDataModelRegistry.put(historyFileName, dataModel);
michael@0 362 }
michael@0 363 return dataModel;
michael@0 364 }
michael@0 365 }
michael@0 366
michael@0 367 /**
michael@0 368 * Creates a new instance.
michael@0 369 *
michael@0 370 * @param context Context for loading resources.
michael@0 371 * @param historyFileName The history XML file.
michael@0 372 */
michael@0 373 private ActivityChooserModel(Context context, String historyFileName) {
michael@0 374 mContext = context.getApplicationContext();
michael@0 375 if (!TextUtils.isEmpty(historyFileName)
michael@0 376 && !historyFileName.endsWith(HISTORY_FILE_EXTENSION)) {
michael@0 377 mHistoryFileName = historyFileName + HISTORY_FILE_EXTENSION;
michael@0 378 } else {
michael@0 379 mHistoryFileName = historyFileName;
michael@0 380 }
michael@0 381
michael@0 382 /**
michael@0 383 * Mozilla: Uses modified receiver
michael@0 384 */
michael@0 385 mPackageMonitor.register(mContext);
michael@0 386 }
michael@0 387
michael@0 388 /**
michael@0 389 * Sets an intent for which to choose a activity.
michael@0 390 * <p>
michael@0 391 * <strong>Note:</strong> Clients must set only semantically similar
michael@0 392 * intents for each data model.
michael@0 393 * <p>
michael@0 394 *
michael@0 395 * @param intent The intent.
michael@0 396 */
michael@0 397 public void setIntent(Intent intent) {
michael@0 398 synchronized (mInstanceLock) {
michael@0 399 if (mIntent == intent) {
michael@0 400 return;
michael@0 401 }
michael@0 402 mIntent = intent;
michael@0 403 mReloadActivities = true;
michael@0 404 ensureConsistentState();
michael@0 405 }
michael@0 406 }
michael@0 407
michael@0 408 /**
michael@0 409 * Gets the intent for which a activity is being chosen.
michael@0 410 *
michael@0 411 * @return The intent.
michael@0 412 */
michael@0 413 public Intent getIntent() {
michael@0 414 synchronized (mInstanceLock) {
michael@0 415 return mIntent;
michael@0 416 }
michael@0 417 }
michael@0 418
michael@0 419 /**
michael@0 420 * Gets the number of activities that can handle the intent.
michael@0 421 *
michael@0 422 * @return The activity count.
michael@0 423 *
michael@0 424 * @see #setIntent(Intent)
michael@0 425 */
michael@0 426 public int getActivityCount() {
michael@0 427 synchronized (mInstanceLock) {
michael@0 428 ensureConsistentState();
michael@0 429 return mActivities.size();
michael@0 430 }
michael@0 431 }
michael@0 432
michael@0 433 /**
michael@0 434 * Gets an activity at a given index.
michael@0 435 *
michael@0 436 * @return The activity.
michael@0 437 *
michael@0 438 * @see ActivityResolveInfo
michael@0 439 * @see #setIntent(Intent)
michael@0 440 */
michael@0 441 public ResolveInfo getActivity(int index) {
michael@0 442 synchronized (mInstanceLock) {
michael@0 443 ensureConsistentState();
michael@0 444 return mActivities.get(index).resolveInfo;
michael@0 445 }
michael@0 446 }
michael@0 447
michael@0 448 /**
michael@0 449 * Gets the index of a the given activity.
michael@0 450 *
michael@0 451 * @param activity The activity index.
michael@0 452 *
michael@0 453 * @return The index if found, -1 otherwise.
michael@0 454 */
michael@0 455 public int getActivityIndex(ResolveInfo activity) {
michael@0 456 synchronized (mInstanceLock) {
michael@0 457 ensureConsistentState();
michael@0 458 List<ActivityResolveInfo> activities = mActivities;
michael@0 459 final int activityCount = activities.size();
michael@0 460 for (int i = 0; i < activityCount; i++) {
michael@0 461 ActivityResolveInfo currentActivity = activities.get(i);
michael@0 462 if (currentActivity.resolveInfo == activity) {
michael@0 463 return i;
michael@0 464 }
michael@0 465 }
michael@0 466 return INVALID_INDEX;
michael@0 467 }
michael@0 468 }
michael@0 469
michael@0 470 /**
michael@0 471 * Chooses a activity to handle the current intent. This will result in
michael@0 472 * adding a historical record for that action and construct intent with
michael@0 473 * its component name set such that it can be immediately started by the
michael@0 474 * client.
michael@0 475 * <p>
michael@0 476 * <strong>Note:</strong> By calling this method the client guarantees
michael@0 477 * that the returned intent will be started. This intent is returned to
michael@0 478 * the client solely to let additional customization before the start.
michael@0 479 * </p>
michael@0 480 *
michael@0 481 * @return An {@link Intent} for launching the activity or null if the
michael@0 482 * policy has consumed the intent or there is not current intent
michael@0 483 * set via {@link #setIntent(Intent)}.
michael@0 484 *
michael@0 485 * @see HistoricalRecord
michael@0 486 * @see OnChooseActivityListener
michael@0 487 */
michael@0 488 public Intent chooseActivity(int index) {
michael@0 489 synchronized (mInstanceLock) {
michael@0 490 if (mIntent == null) {
michael@0 491 return null;
michael@0 492 }
michael@0 493
michael@0 494 ensureConsistentState();
michael@0 495
michael@0 496 ActivityResolveInfo chosenActivity = mActivities.get(index);
michael@0 497
michael@0 498 ComponentName chosenName = new ComponentName(
michael@0 499 chosenActivity.resolveInfo.activityInfo.packageName,
michael@0 500 chosenActivity.resolveInfo.activityInfo.name);
michael@0 501
michael@0 502 Intent choiceIntent = new Intent(mIntent);
michael@0 503 choiceIntent.setComponent(chosenName);
michael@0 504
michael@0 505 if (mActivityChoserModelPolicy != null) {
michael@0 506 // Do not allow the policy to change the intent.
michael@0 507 Intent choiceIntentCopy = new Intent(choiceIntent);
michael@0 508 final boolean handled = mActivityChoserModelPolicy.onChooseActivity(this,
michael@0 509 choiceIntentCopy);
michael@0 510 if (handled) {
michael@0 511 return null;
michael@0 512 }
michael@0 513 }
michael@0 514
michael@0 515 HistoricalRecord historicalRecord = new HistoricalRecord(chosenName,
michael@0 516 System.currentTimeMillis(), DEFAULT_HISTORICAL_RECORD_WEIGHT);
michael@0 517 addHistoricalRecord(historicalRecord);
michael@0 518
michael@0 519 return choiceIntent;
michael@0 520 }
michael@0 521 }
michael@0 522
michael@0 523 /**
michael@0 524 * Sets the listener for choosing an activity.
michael@0 525 *
michael@0 526 * @param listener The listener.
michael@0 527 */
michael@0 528 public void setOnChooseActivityListener(OnChooseActivityListener listener) {
michael@0 529 synchronized (mInstanceLock) {
michael@0 530 mActivityChoserModelPolicy = listener;
michael@0 531 }
michael@0 532 }
michael@0 533
michael@0 534 /**
michael@0 535 * Gets the default activity, The default activity is defined as the one
michael@0 536 * with highest rank i.e. the first one in the list of activities that can
michael@0 537 * handle the intent.
michael@0 538 *
michael@0 539 * @return The default activity, <code>null</code> id not activities.
michael@0 540 *
michael@0 541 * @see #getActivity(int)
michael@0 542 */
michael@0 543 public ResolveInfo getDefaultActivity() {
michael@0 544 synchronized (mInstanceLock) {
michael@0 545 ensureConsistentState();
michael@0 546 if (!mActivities.isEmpty()) {
michael@0 547 return mActivities.get(0).resolveInfo;
michael@0 548 }
michael@0 549 }
michael@0 550 return null;
michael@0 551 }
michael@0 552
michael@0 553 /**
michael@0 554 * Sets the default activity. The default activity is set by adding a
michael@0 555 * historical record with weight high enough that this activity will
michael@0 556 * become the highest ranked. Such a strategy guarantees that the default
michael@0 557 * will eventually change if not used. Also the weight of the record for
michael@0 558 * setting a default is inflated with a constant amount to guarantee that
michael@0 559 * it will stay as default for awhile.
michael@0 560 *
michael@0 561 * @param index The index of the activity to set as default.
michael@0 562 */
michael@0 563 public void setDefaultActivity(int index) {
michael@0 564 synchronized (mInstanceLock) {
michael@0 565 ensureConsistentState();
michael@0 566
michael@0 567 ActivityResolveInfo newDefaultActivity = mActivities.get(index);
michael@0 568 ActivityResolveInfo oldDefaultActivity = mActivities.get(0);
michael@0 569
michael@0 570 final float weight;
michael@0 571 if (oldDefaultActivity != null) {
michael@0 572 // Add a record with weight enough to boost the chosen at the top.
michael@0 573 weight = oldDefaultActivity.weight - newDefaultActivity.weight
michael@0 574 + DEFAULT_ACTIVITY_INFLATION;
michael@0 575 } else {
michael@0 576 weight = DEFAULT_HISTORICAL_RECORD_WEIGHT;
michael@0 577 }
michael@0 578
michael@0 579 ComponentName defaultName = new ComponentName(
michael@0 580 newDefaultActivity.resolveInfo.activityInfo.packageName,
michael@0 581 newDefaultActivity.resolveInfo.activityInfo.name);
michael@0 582 HistoricalRecord historicalRecord = new HistoricalRecord(defaultName,
michael@0 583 System.currentTimeMillis(), weight);
michael@0 584 addHistoricalRecord(historicalRecord);
michael@0 585 }
michael@0 586 }
michael@0 587
michael@0 588 /**
michael@0 589 * Persists the history data to the backing file if the latter
michael@0 590 * was provided. Calling this method before a call to {@link #readHistoricalDataIfNeeded()}
michael@0 591 * throws an exception. Calling this method more than one without choosing an
michael@0 592 * activity has not effect.
michael@0 593 *
michael@0 594 * @throws IllegalStateException If this method is called before a call to
michael@0 595 * {@link #readHistoricalDataIfNeeded()}.
michael@0 596 */
michael@0 597 private void persistHistoricalDataIfNeeded() {
michael@0 598 if (!mReadShareHistoryCalled) {
michael@0 599 throw new IllegalStateException("No preceding call to #readHistoricalData");
michael@0 600 }
michael@0 601 if (!mHistoricalRecordsChanged) {
michael@0 602 return;
michael@0 603 }
michael@0 604 mHistoricalRecordsChanged = false;
michael@0 605 if (!TextUtils.isEmpty(mHistoryFileName)) {
michael@0 606 /**
michael@0 607 * Mozilla: Converted to a normal task.execute call so that this works on < ICS phones.
michael@0 608 */
michael@0 609 new PersistHistoryAsyncTask().execute(new ArrayList<HistoricalRecord>(mHistoricalRecords), mHistoryFileName);
michael@0 610 }
michael@0 611 }
michael@0 612
michael@0 613 /**
michael@0 614 * Sets the sorter for ordering activities based on historical data and an intent.
michael@0 615 *
michael@0 616 * @param activitySorter The sorter.
michael@0 617 *
michael@0 618 * @see ActivitySorter
michael@0 619 */
michael@0 620 public void setActivitySorter(ActivitySorter activitySorter) {
michael@0 621 synchronized (mInstanceLock) {
michael@0 622 if (mActivitySorter == activitySorter) {
michael@0 623 return;
michael@0 624 }
michael@0 625 mActivitySorter = activitySorter;
michael@0 626 if (sortActivitiesIfNeeded()) {
michael@0 627 notifyChanged();
michael@0 628 }
michael@0 629 }
michael@0 630 }
michael@0 631
michael@0 632 /**
michael@0 633 * Sets the maximal size of the historical data. Defaults to
michael@0 634 * {@link #DEFAULT_HISTORY_MAX_LENGTH}
michael@0 635 * <p>
michael@0 636 * <strong>Note:</strong> Setting this property will immediately
michael@0 637 * enforce the specified max history size by dropping enough old
michael@0 638 * historical records to enforce the desired size. Thus, any
michael@0 639 * records that exceed the history size will be discarded and
michael@0 640 * irreversibly lost.
michael@0 641 * </p>
michael@0 642 *
michael@0 643 * @param historyMaxSize The max history size.
michael@0 644 */
michael@0 645 public void setHistoryMaxSize(int historyMaxSize) {
michael@0 646 synchronized (mInstanceLock) {
michael@0 647 if (mHistoryMaxSize == historyMaxSize) {
michael@0 648 return;
michael@0 649 }
michael@0 650 mHistoryMaxSize = historyMaxSize;
michael@0 651 pruneExcessiveHistoricalRecordsIfNeeded();
michael@0 652 if (sortActivitiesIfNeeded()) {
michael@0 653 notifyChanged();
michael@0 654 }
michael@0 655 }
michael@0 656 }
michael@0 657
michael@0 658 /**
michael@0 659 * Gets the history max size.
michael@0 660 *
michael@0 661 * @return The history max size.
michael@0 662 */
michael@0 663 public int getHistoryMaxSize() {
michael@0 664 synchronized (mInstanceLock) {
michael@0 665 return mHistoryMaxSize;
michael@0 666 }
michael@0 667 }
michael@0 668
michael@0 669 /**
michael@0 670 * Gets the history size.
michael@0 671 *
michael@0 672 * @return The history size.
michael@0 673 */
michael@0 674 public int getHistorySize() {
michael@0 675 synchronized (mInstanceLock) {
michael@0 676 ensureConsistentState();
michael@0 677 return mHistoricalRecords.size();
michael@0 678 }
michael@0 679 }
michael@0 680
michael@0 681 public int getDistinctActivityCountInHistory() {
michael@0 682 synchronized (mInstanceLock) {
michael@0 683 ensureConsistentState();
michael@0 684 final List<String> packages = new ArrayList<String>();
michael@0 685 for (HistoricalRecord record : mHistoricalRecords) {
michael@0 686 String activity = record.activity.flattenToString();
michael@0 687 if (!packages.contains(activity)) {
michael@0 688 packages.add(activity);
michael@0 689 }
michael@0 690 }
michael@0 691 return packages.size();
michael@0 692 }
michael@0 693 }
michael@0 694
michael@0 695 @Override
michael@0 696 protected void finalize() throws Throwable {
michael@0 697 super.finalize();
michael@0 698
michael@0 699 /**
michael@0 700 * Mozilla: Not needed for the application.
michael@0 701 */
michael@0 702 mPackageMonitor.unregister();
michael@0 703 }
michael@0 704
michael@0 705 /**
michael@0 706 * Ensures the model is in a consistent state which is the
michael@0 707 * activities for the current intent have been loaded, the
michael@0 708 * most recent history has been read, and the activities
michael@0 709 * are sorted.
michael@0 710 */
michael@0 711 private void ensureConsistentState() {
michael@0 712 boolean stateChanged = loadActivitiesIfNeeded();
michael@0 713 stateChanged |= readHistoricalDataIfNeeded();
michael@0 714 pruneExcessiveHistoricalRecordsIfNeeded();
michael@0 715 if (stateChanged) {
michael@0 716 sortActivitiesIfNeeded();
michael@0 717 notifyChanged();
michael@0 718 }
michael@0 719 }
michael@0 720
michael@0 721 /**
michael@0 722 * Sorts the activities if necessary which is if there is a
michael@0 723 * sorter, there are some activities to sort, and there is some
michael@0 724 * historical data.
michael@0 725 *
michael@0 726 * @return Whether sorting was performed.
michael@0 727 */
michael@0 728 private boolean sortActivitiesIfNeeded() {
michael@0 729 if (mActivitySorter != null && mIntent != null
michael@0 730 && !mActivities.isEmpty() && !mHistoricalRecords.isEmpty()) {
michael@0 731 mActivitySorter.sort(mIntent, mActivities,
michael@0 732 Collections.unmodifiableList(mHistoricalRecords));
michael@0 733 return true;
michael@0 734 }
michael@0 735 return false;
michael@0 736 }
michael@0 737
michael@0 738 /**
michael@0 739 * Loads the activities for the current intent if needed which is
michael@0 740 * if they are not already loaded for the current intent.
michael@0 741 *
michael@0 742 * @return Whether loading was performed.
michael@0 743 */
michael@0 744 private boolean loadActivitiesIfNeeded() {
michael@0 745 if (mReloadActivities && mIntent != null) {
michael@0 746 mReloadActivities = false;
michael@0 747 mActivities.clear();
michael@0 748 List<ResolveInfo> resolveInfos = mContext.getPackageManager()
michael@0 749 .queryIntentActivities(mIntent, 0);
michael@0 750 final int resolveInfoCount = resolveInfos.size();
michael@0 751 for (int i = 0; i < resolveInfoCount; i++) {
michael@0 752 ResolveInfo resolveInfo = resolveInfos.get(i);
michael@0 753 mActivities.add(new ActivityResolveInfo(resolveInfo));
michael@0 754 }
michael@0 755 return true;
michael@0 756 }
michael@0 757 return false;
michael@0 758 }
michael@0 759
michael@0 760 /**
michael@0 761 * Reads the historical data if necessary which is it has
michael@0 762 * changed, there is a history file, and there is not persist
michael@0 763 * in progress.
michael@0 764 *
michael@0 765 * @return Whether reading was performed.
michael@0 766 */
michael@0 767 private boolean readHistoricalDataIfNeeded() {
michael@0 768 if (mCanReadHistoricalData && mHistoricalRecordsChanged &&
michael@0 769 !TextUtils.isEmpty(mHistoryFileName)) {
michael@0 770 mCanReadHistoricalData = false;
michael@0 771 mReadShareHistoryCalled = true;
michael@0 772 readHistoricalDataImpl();
michael@0 773 return true;
michael@0 774 }
michael@0 775 return false;
michael@0 776 }
michael@0 777
michael@0 778 /**
michael@0 779 * Adds a historical record.
michael@0 780 *
michael@0 781 * @param historicalRecord The record to add.
michael@0 782 * @return True if the record was added.
michael@0 783 */
michael@0 784 private boolean addHistoricalRecord(HistoricalRecord historicalRecord) {
michael@0 785 final boolean added = mHistoricalRecords.add(historicalRecord);
michael@0 786 if (added) {
michael@0 787 mHistoricalRecordsChanged = true;
michael@0 788 pruneExcessiveHistoricalRecordsIfNeeded();
michael@0 789 persistHistoricalDataIfNeeded();
michael@0 790 sortActivitiesIfNeeded();
michael@0 791 notifyChanged();
michael@0 792 }
michael@0 793 return added;
michael@0 794 }
michael@0 795
michael@0 796 /**
michael@0 797 * Removes all historical records for this pkg.
michael@0 798 *
michael@0 799 * @param historicalRecord The pkg to delete records for.
michael@0 800 * @return True if the record was added.
michael@0 801 */
michael@0 802 private boolean removeHistoricalRecordsForPackage(final String pkg) {
michael@0 803 boolean removed = false;
michael@0 804
michael@0 805 for (Iterator<HistoricalRecord> i = mHistoricalRecords.iterator(); i.hasNext();) {
michael@0 806 final HistoricalRecord record = i.next();
michael@0 807 if (record.activity.getPackageName().equals(pkg)) {
michael@0 808 i.remove();
michael@0 809 removed = true;
michael@0 810 }
michael@0 811 }
michael@0 812
michael@0 813 if (removed) {
michael@0 814 mHistoricalRecordsChanged = true;
michael@0 815 pruneExcessiveHistoricalRecordsIfNeeded();
michael@0 816 persistHistoricalDataIfNeeded();
michael@0 817 sortActivitiesIfNeeded();
michael@0 818 notifyChanged();
michael@0 819 }
michael@0 820
michael@0 821 return removed;
michael@0 822 }
michael@0 823
michael@0 824 /**
michael@0 825 * Prunes older excessive records to guarantee maxHistorySize.
michael@0 826 */
michael@0 827 private void pruneExcessiveHistoricalRecordsIfNeeded() {
michael@0 828 final int pruneCount = mHistoricalRecords.size() - mHistoryMaxSize;
michael@0 829 if (pruneCount <= 0) {
michael@0 830 return;
michael@0 831 }
michael@0 832 mHistoricalRecordsChanged = true;
michael@0 833 for (int i = 0; i < pruneCount; i++) {
michael@0 834 HistoricalRecord prunedRecord = mHistoricalRecords.remove(0);
michael@0 835 if (DEBUG) {
michael@0 836 Log.i(LOG_TAG, "Pruned: " + prunedRecord);
michael@0 837 }
michael@0 838 }
michael@0 839 }
michael@0 840
michael@0 841 /**
michael@0 842 * Represents a record in the history.
michael@0 843 */
michael@0 844 public final static class HistoricalRecord {
michael@0 845
michael@0 846 /**
michael@0 847 * The activity name.
michael@0 848 */
michael@0 849 public final ComponentName activity;
michael@0 850
michael@0 851 /**
michael@0 852 * The choice time.
michael@0 853 */
michael@0 854 public final long time;
michael@0 855
michael@0 856 /**
michael@0 857 * The record weight.
michael@0 858 */
michael@0 859 public final float weight;
michael@0 860
michael@0 861 /**
michael@0 862 * Creates a new instance.
michael@0 863 *
michael@0 864 * @param activityName The activity component name flattened to string.
michael@0 865 * @param time The time the activity was chosen.
michael@0 866 * @param weight The weight of the record.
michael@0 867 */
michael@0 868 public HistoricalRecord(String activityName, long time, float weight) {
michael@0 869 this(ComponentName.unflattenFromString(activityName), time, weight);
michael@0 870 }
michael@0 871
michael@0 872 /**
michael@0 873 * Creates a new instance.
michael@0 874 *
michael@0 875 * @param activityName The activity name.
michael@0 876 * @param time The time the activity was chosen.
michael@0 877 * @param weight The weight of the record.
michael@0 878 */
michael@0 879 public HistoricalRecord(ComponentName activityName, long time, float weight) {
michael@0 880 this.activity = activityName;
michael@0 881 this.time = time;
michael@0 882 this.weight = weight;
michael@0 883 }
michael@0 884
michael@0 885 @Override
michael@0 886 public int hashCode() {
michael@0 887 final int prime = 31;
michael@0 888 int result = 1;
michael@0 889 result = prime * result + ((activity == null) ? 0 : activity.hashCode());
michael@0 890 result = prime * result + (int) (time ^ (time >>> 32));
michael@0 891 result = prime * result + Float.floatToIntBits(weight);
michael@0 892 return result;
michael@0 893 }
michael@0 894
michael@0 895 @Override
michael@0 896 public boolean equals(Object obj) {
michael@0 897 if (this == obj) {
michael@0 898 return true;
michael@0 899 }
michael@0 900 if (obj == null) {
michael@0 901 return false;
michael@0 902 }
michael@0 903 if (getClass() != obj.getClass()) {
michael@0 904 return false;
michael@0 905 }
michael@0 906 HistoricalRecord other = (HistoricalRecord) obj;
michael@0 907 if (activity == null) {
michael@0 908 if (other.activity != null) {
michael@0 909 return false;
michael@0 910 }
michael@0 911 } else if (!activity.equals(other.activity)) {
michael@0 912 return false;
michael@0 913 }
michael@0 914 if (time != other.time) {
michael@0 915 return false;
michael@0 916 }
michael@0 917 if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) {
michael@0 918 return false;
michael@0 919 }
michael@0 920 return true;
michael@0 921 }
michael@0 922
michael@0 923 @Override
michael@0 924 public String toString() {
michael@0 925 StringBuilder builder = new StringBuilder();
michael@0 926 builder.append("[");
michael@0 927 builder.append("; activity:").append(activity);
michael@0 928 builder.append("; time:").append(time);
michael@0 929 builder.append("; weight:").append(new BigDecimal(weight));
michael@0 930 builder.append("]");
michael@0 931 return builder.toString();
michael@0 932 }
michael@0 933 }
michael@0 934
michael@0 935 /**
michael@0 936 * Represents an activity.
michael@0 937 */
michael@0 938 public final class ActivityResolveInfo implements Comparable<ActivityResolveInfo> {
michael@0 939
michael@0 940 /**
michael@0 941 * The {@link ResolveInfo} of the activity.
michael@0 942 */
michael@0 943 public final ResolveInfo resolveInfo;
michael@0 944
michael@0 945 /**
michael@0 946 * Weight of the activity. Useful for sorting.
michael@0 947 */
michael@0 948 public float weight;
michael@0 949
michael@0 950 /**
michael@0 951 * Creates a new instance.
michael@0 952 *
michael@0 953 * @param resolveInfo activity {@link ResolveInfo}.
michael@0 954 */
michael@0 955 public ActivityResolveInfo(ResolveInfo resolveInfo) {
michael@0 956 this.resolveInfo = resolveInfo;
michael@0 957 }
michael@0 958
michael@0 959 @Override
michael@0 960 public int hashCode() {
michael@0 961 return 31 + Float.floatToIntBits(weight);
michael@0 962 }
michael@0 963
michael@0 964 @Override
michael@0 965 public boolean equals(Object obj) {
michael@0 966 if (this == obj) {
michael@0 967 return true;
michael@0 968 }
michael@0 969 if (obj == null) {
michael@0 970 return false;
michael@0 971 }
michael@0 972 if (getClass() != obj.getClass()) {
michael@0 973 return false;
michael@0 974 }
michael@0 975 ActivityResolveInfo other = (ActivityResolveInfo) obj;
michael@0 976 if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) {
michael@0 977 return false;
michael@0 978 }
michael@0 979 return true;
michael@0 980 }
michael@0 981
michael@0 982 public int compareTo(ActivityResolveInfo another) {
michael@0 983 return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight);
michael@0 984 }
michael@0 985
michael@0 986 @Override
michael@0 987 public String toString() {
michael@0 988 StringBuilder builder = new StringBuilder();
michael@0 989 builder.append("[");
michael@0 990 builder.append("resolveInfo:").append(resolveInfo.toString());
michael@0 991 builder.append("; weight:").append(new BigDecimal(weight));
michael@0 992 builder.append("]");
michael@0 993 return builder.toString();
michael@0 994 }
michael@0 995 }
michael@0 996
michael@0 997 /**
michael@0 998 * Default activity sorter implementation.
michael@0 999 */
michael@0 1000 private final class DefaultSorter implements ActivitySorter {
michael@0 1001 private static final float WEIGHT_DECAY_COEFFICIENT = 0.95f;
michael@0 1002
michael@0 1003 private final Map<String, ActivityResolveInfo> mPackageNameToActivityMap =
michael@0 1004 new HashMap<String, ActivityResolveInfo>();
michael@0 1005
michael@0 1006 public void sort(Intent intent, List<ActivityResolveInfo> activities,
michael@0 1007 List<HistoricalRecord> historicalRecords) {
michael@0 1008 Map<String, ActivityResolveInfo> packageNameToActivityMap =
michael@0 1009 mPackageNameToActivityMap;
michael@0 1010 packageNameToActivityMap.clear();
michael@0 1011
michael@0 1012 final int activityCount = activities.size();
michael@0 1013 for (int i = 0; i < activityCount; i++) {
michael@0 1014 ActivityResolveInfo activity = activities.get(i);
michael@0 1015 activity.weight = 0.0f;
michael@0 1016
michael@0 1017 // Make sure we're using a non-ambiguous name here
michael@0 1018 ComponentName chosenName = new ComponentName(
michael@0 1019 activity.resolveInfo.activityInfo.packageName,
michael@0 1020 activity.resolveInfo.activityInfo.name);
michael@0 1021 String packageName = chosenName.flattenToString();
michael@0 1022 packageNameToActivityMap.put(packageName, activity);
michael@0 1023 }
michael@0 1024
michael@0 1025 final int lastShareIndex = historicalRecords.size() - 1;
michael@0 1026 float nextRecordWeight = 1;
michael@0 1027 for (int i = lastShareIndex; i >= 0; i--) {
michael@0 1028 HistoricalRecord historicalRecord = historicalRecords.get(i);
michael@0 1029 String packageName = historicalRecord.activity.flattenToString();
michael@0 1030 ActivityResolveInfo activity = packageNameToActivityMap.get(packageName);
michael@0 1031 if (activity != null) {
michael@0 1032 activity.weight += historicalRecord.weight * nextRecordWeight;
michael@0 1033 nextRecordWeight = nextRecordWeight * WEIGHT_DECAY_COEFFICIENT;
michael@0 1034 }
michael@0 1035 }
michael@0 1036
michael@0 1037 Collections.sort(activities);
michael@0 1038
michael@0 1039 if (DEBUG) {
michael@0 1040 for (int i = 0; i < activityCount; i++) {
michael@0 1041 Log.i(LOG_TAG, "Sorted: " + activities.get(i));
michael@0 1042 }
michael@0 1043 }
michael@0 1044 }
michael@0 1045 }
michael@0 1046
michael@0 1047 /**
michael@0 1048 * Command for reading the historical records from a file off the UI thread.
michael@0 1049 */
michael@0 1050 private void readHistoricalDataImpl() {
michael@0 1051 FileInputStream fis = null;
michael@0 1052 try {
michael@0 1053 GeckoProfile profile = GeckoProfile.get(mContext);
michael@0 1054 File f = profile.getFile(mHistoryFileName);
michael@0 1055 if (!f.exists()) {
michael@0 1056 // Fall back to the non-profile aware file if it exists...
michael@0 1057 File oldFile = new File(mHistoryFileName);
michael@0 1058 oldFile.renameTo(f);
michael@0 1059 }
michael@0 1060 fis = new FileInputStream(f);
michael@0 1061 } catch (FileNotFoundException fnfe) {
michael@0 1062 try {
michael@0 1063 Distribution dist = new Distribution(mContext);
michael@0 1064 File distFile = dist.getDistributionFile("quickshare/" + mHistoryFileName);
michael@0 1065 if (distFile == null) {
michael@0 1066 if (DEBUG) {
michael@0 1067 Log.i(LOG_TAG, "Could not open historical records file: " + mHistoryFileName);
michael@0 1068 }
michael@0 1069 return;
michael@0 1070 }
michael@0 1071 fis = new FileInputStream(distFile);
michael@0 1072 } catch(Exception ex) {
michael@0 1073 if (DEBUG) {
michael@0 1074 Log.i(LOG_TAG, "Could not open historical records file: " + mHistoryFileName);
michael@0 1075 }
michael@0 1076 return;
michael@0 1077 }
michael@0 1078 }
michael@0 1079
michael@0 1080 try {
michael@0 1081 XmlPullParser parser = Xml.newPullParser();
michael@0 1082 parser.setInput(fis, null);
michael@0 1083
michael@0 1084 int type = XmlPullParser.START_DOCUMENT;
michael@0 1085 while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) {
michael@0 1086 type = parser.next();
michael@0 1087 }
michael@0 1088
michael@0 1089 if (!TAG_HISTORICAL_RECORDS.equals(parser.getName())) {
michael@0 1090 throw new XmlPullParserException("Share records file does not start with "
michael@0 1091 + TAG_HISTORICAL_RECORDS + " tag.");
michael@0 1092 }
michael@0 1093
michael@0 1094 List<HistoricalRecord> historicalRecords = mHistoricalRecords;
michael@0 1095 historicalRecords.clear();
michael@0 1096
michael@0 1097 while (true) {
michael@0 1098 type = parser.next();
michael@0 1099 if (type == XmlPullParser.END_DOCUMENT) {
michael@0 1100 break;
michael@0 1101 }
michael@0 1102 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
michael@0 1103 continue;
michael@0 1104 }
michael@0 1105 String nodeName = parser.getName();
michael@0 1106 if (!TAG_HISTORICAL_RECORD.equals(nodeName)) {
michael@0 1107 throw new XmlPullParserException("Share records file not well-formed.");
michael@0 1108 }
michael@0 1109
michael@0 1110 String activity = parser.getAttributeValue(null, ATTRIBUTE_ACTIVITY);
michael@0 1111 final long time =
michael@0 1112 Long.parseLong(parser.getAttributeValue(null, ATTRIBUTE_TIME));
michael@0 1113 final float weight =
michael@0 1114 Float.parseFloat(parser.getAttributeValue(null, ATTRIBUTE_WEIGHT));
michael@0 1115 HistoricalRecord readRecord = new HistoricalRecord(activity, time, weight);
michael@0 1116 historicalRecords.add(readRecord);
michael@0 1117
michael@0 1118 if (DEBUG) {
michael@0 1119 Log.i(LOG_TAG, "Read " + readRecord.toString());
michael@0 1120 }
michael@0 1121 }
michael@0 1122
michael@0 1123 if (DEBUG) {
michael@0 1124 Log.i(LOG_TAG, "Read " + historicalRecords.size() + " historical records.");
michael@0 1125 }
michael@0 1126 } catch (XmlPullParserException xppe) {
michael@0 1127 Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, xppe);
michael@0 1128 } catch (IOException ioe) {
michael@0 1129 Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, ioe);
michael@0 1130 } finally {
michael@0 1131 if (fis != null) {
michael@0 1132 try {
michael@0 1133 fis.close();
michael@0 1134 } catch (IOException ioe) {
michael@0 1135 /* ignore */
michael@0 1136 }
michael@0 1137 }
michael@0 1138 }
michael@0 1139 }
michael@0 1140
michael@0 1141 /**
michael@0 1142 * Command for persisting the historical records to a file off the UI thread.
michael@0 1143 */
michael@0 1144 private final class PersistHistoryAsyncTask extends AsyncTask<Object, Void, Void> {
michael@0 1145
michael@0 1146 @Override
michael@0 1147 @SuppressWarnings("unchecked")
michael@0 1148 public Void doInBackground(Object... args) {
michael@0 1149 List<HistoricalRecord> historicalRecords = (List<HistoricalRecord>) args[0];
michael@0 1150 String historyFileName = (String) args[1];
michael@0 1151
michael@0 1152 FileOutputStream fos = null;
michael@0 1153
michael@0 1154 try {
michael@0 1155 // Mozilla - Update the location we save files to
michael@0 1156 GeckoProfile profile = GeckoProfile.get(mContext);
michael@0 1157 File file = profile.getFile(historyFileName);
michael@0 1158 fos = new FileOutputStream(file);
michael@0 1159 } catch (FileNotFoundException fnfe) {
michael@0 1160 Log.e(LOG_TAG, "Error writing historical record file: " + historyFileName, fnfe);
michael@0 1161 return null;
michael@0 1162 }
michael@0 1163
michael@0 1164 XmlSerializer serializer = Xml.newSerializer();
michael@0 1165
michael@0 1166 try {
michael@0 1167 serializer.setOutput(fos, null);
michael@0 1168 serializer.startDocument("UTF-8", true);
michael@0 1169 serializer.startTag(null, TAG_HISTORICAL_RECORDS);
michael@0 1170
michael@0 1171 final int recordCount = historicalRecords.size();
michael@0 1172 for (int i = 0; i < recordCount; i++) {
michael@0 1173 HistoricalRecord record = historicalRecords.remove(0);
michael@0 1174 serializer.startTag(null, TAG_HISTORICAL_RECORD);
michael@0 1175 serializer.attribute(null, ATTRIBUTE_ACTIVITY,
michael@0 1176 record.activity.flattenToString());
michael@0 1177 serializer.attribute(null, ATTRIBUTE_TIME, String.valueOf(record.time));
michael@0 1178 serializer.attribute(null, ATTRIBUTE_WEIGHT, String.valueOf(record.weight));
michael@0 1179 serializer.endTag(null, TAG_HISTORICAL_RECORD);
michael@0 1180 if (DEBUG) {
michael@0 1181 Log.i(LOG_TAG, "Wrote " + record.toString());
michael@0 1182 }
michael@0 1183 }
michael@0 1184
michael@0 1185 serializer.endTag(null, TAG_HISTORICAL_RECORDS);
michael@0 1186 serializer.endDocument();
michael@0 1187
michael@0 1188 if (DEBUG) {
michael@0 1189 Log.i(LOG_TAG, "Wrote " + recordCount + " historical records.");
michael@0 1190 }
michael@0 1191 } catch (IllegalArgumentException iae) {
michael@0 1192 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, iae);
michael@0 1193 } catch (IllegalStateException ise) {
michael@0 1194 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ise);
michael@0 1195 } catch (IOException ioe) {
michael@0 1196 Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ioe);
michael@0 1197 } finally {
michael@0 1198 mCanReadHistoricalData = true;
michael@0 1199 if (fos != null) {
michael@0 1200 try {
michael@0 1201 fos.close();
michael@0 1202 } catch (IOException e) {
michael@0 1203 /* ignore */
michael@0 1204 }
michael@0 1205 }
michael@0 1206 }
michael@0 1207 return null;
michael@0 1208 }
michael@0 1209 }
michael@0 1210
michael@0 1211 /**
michael@0 1212 * Keeps in sync the historical records and activities with the installed applications.
michael@0 1213 */
michael@0 1214 /**
michael@0 1215 * Mozilla: Adapted significantly
michael@0 1216 */
michael@0 1217 private static final String LOGTAG = "GeckoActivityChooserModel";
michael@0 1218 private final class DataModelPackageMonitor extends BroadcastReceiver {
michael@0 1219 private Context mContext;
michael@0 1220
michael@0 1221 public DataModelPackageMonitor() { }
michael@0 1222
michael@0 1223 public void register(Context context) {
michael@0 1224 mContext = context;
michael@0 1225
michael@0 1226 String[] intents = new String[] {
michael@0 1227 Intent.ACTION_PACKAGE_REMOVED,
michael@0 1228 Intent.ACTION_PACKAGE_ADDED,
michael@0 1229 Intent.ACTION_PACKAGE_CHANGED
michael@0 1230 };
michael@0 1231
michael@0 1232 for (String intent : intents) {
michael@0 1233 IntentFilter removeFilter = new IntentFilter(intent);
michael@0 1234 removeFilter.addDataScheme("package");
michael@0 1235 context.registerReceiver(this, removeFilter);
michael@0 1236 }
michael@0 1237 }
michael@0 1238
michael@0 1239 public void unregister() {
michael@0 1240 mContext.unregisterReceiver(this);
michael@0 1241 mContext = null;
michael@0 1242 }
michael@0 1243
michael@0 1244 @Override
michael@0 1245 public void onReceive(Context context, Intent intent) {
michael@0 1246 String action = intent.getAction();
michael@0 1247 if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
michael@0 1248 String packageName = intent.getData().getSchemeSpecificPart();
michael@0 1249 removeHistoricalRecordsForPackage(packageName);
michael@0 1250 }
michael@0 1251
michael@0 1252 mReloadActivities = true;
michael@0 1253 }
michael@0 1254 }
michael@0 1255 }
michael@0 1256

mercurial