OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 package org.chromium.chrome.browser.document; |
| 6 |
| 7 import android.annotation.TargetApi; |
| 8 import android.app.Activity; |
| 9 import android.app.ActivityManager; |
| 10 import android.app.ActivityManager.AppTask; |
| 11 import android.app.ActivityManager.TaskDescription; |
| 12 import android.content.Context; |
| 13 import android.content.Intent; |
| 14 import android.graphics.Bitmap; |
| 15 import android.graphics.Bitmap.Config; |
| 16 import android.graphics.Canvas; |
| 17 import android.graphics.Color; |
| 18 import android.os.Build; |
| 19 import android.text.TextUtils; |
| 20 import android.util.Log; |
| 21 import android.util.Pair; |
| 22 import android.util.SparseArray; |
| 23 |
| 24 import com.google.android.apps.chrome.R; |
| 25 |
| 26 import org.chromium.base.ApplicationStatus; |
| 27 import org.chromium.base.ImportantFileWriterAndroid; |
| 28 import org.chromium.chrome.browser.ApplicationLifetime; |
| 29 import org.chromium.chrome.browser.ChromeMobileApplication; |
| 30 import org.chromium.chrome.browser.IntentHandler; |
| 31 import org.chromium.chrome.browser.Tab; |
| 32 import org.chromium.chrome.browser.TabState; |
| 33 import org.chromium.chrome.browser.UrlConstants; |
| 34 import org.chromium.chrome.browser.UrlUtilities; |
| 35 import org.chromium.chrome.browser.compositor.layouts.content.ContentOffsetProvi
der; |
| 36 import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager; |
| 37 import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager.
DecompressThumbnailCallback; |
| 38 import org.chromium.chrome.browser.device.DeviceClassManager; |
| 39 import org.chromium.chrome.browser.favicon.FaviconHelper; |
| 40 import org.chromium.chrome.browser.favicon.FaviconHelper.FaviconImageCallback; |
| 41 import org.chromium.chrome.browser.ntp.NativePageFactory; |
| 42 import org.chromium.chrome.browser.preferences.ChromePreferenceManager; |
| 43 import org.chromium.chrome.browser.profiles.Profile; |
| 44 import org.chromium.chrome.browser.tabmodel.TabPersistentStore; |
| 45 import org.chromium.chrome.browser.tabmodel.TabPersistentStore.OnTabStateReadCal
lback; |
| 46 import org.chromium.chrome.browser.tabmodel.document.ActivityDelegate; |
| 47 import org.chromium.chrome.browser.tabmodel.document.DocumentTabModel; |
| 48 import org.chromium.chrome.browser.tabmodel.document.DocumentTabModel.Entry; |
| 49 import org.chromium.chrome.browser.tabmodel.document.DocumentTabModel.Initializa
tionObserver; |
| 50 import org.chromium.chrome.browser.tabmodel.document.DocumentTabModelImpl; |
| 51 import org.chromium.chrome.browser.tabmodel.document.DocumentTabModelSelector; |
| 52 import org.chromium.chrome.browser.tabmodel.document.OffTheRecordDocumentTabMode
l; |
| 53 import org.chromium.chrome.browser.tabmodel.document.StorageDelegate; |
| 54 |
| 55 import java.io.File; |
| 56 import java.io.IOException; |
| 57 import java.util.ArrayList; |
| 58 import java.util.List; |
| 59 |
| 60 /** |
| 61 * The class that carries out migration of tab states from/to document mode. |
| 62 */ |
| 63 @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 64 public class DocumentMigrationHelper { |
| 65 private static final String TAG = "DocumentMigrationHelper"; |
| 66 private static final int[] ICON_TYPES = {FaviconHelper.FAVICON, |
| 67 FaviconHelper.TOUCH_ICON | FaviconHelper.TOUCH_PRECOMPOSED_ICON}; |
| 68 private static final int DESIRED_ICON_SIZE_DP = 32; |
| 69 |
| 70 public static final int FINALIZE_MODE_NO_ACTION = 0; |
| 71 public static final int FINALIZE_MODE_FINISH_ACTIVITY = 1; |
| 72 public static final int FINALIZE_MODE_RESTART_APP = 2; |
| 73 |
| 74 private static class MigrationTabStateReadCallback implements OnTabStateRead
Callback { |
| 75 private int mSelectedTabId = Tab.INVALID_TAB_ID; |
| 76 |
| 77 @Override |
| 78 public void onDetailsRead(int index, int id, String url, boolean isStand
ardActiveIndex, |
| 79 boolean isIncognitoActiveIndex) { |
| 80 Tab.incrementIdCounterTo(id + 1); |
| 81 if (!isStandardActiveIndex) return; |
| 82 // If the current tab read is the active standard tab, set the last
used |
| 83 // tab pref with the id, so that when document mode starts we show t
hat |
| 84 // tab first. |
| 85 mSelectedTabId = id; |
| 86 } |
| 87 |
| 88 public int getSelectedTabId() { |
| 89 return mSelectedTabId; |
| 90 } |
| 91 } |
| 92 |
| 93 /** |
| 94 * Stores a list of "tasks" that are meant to be returned by the ActivityMan
ager for migration. |
| 95 * The tasks are inserted manually during the migration from classic mode to
document mode. |
| 96 */ |
| 97 private static class MigrationActivityDelegate extends ActivityDelegate { |
| 98 private final List<Entry> mEntries; |
| 99 private final int mSelectedTabId; |
| 100 |
| 101 private MigrationActivityDelegate(List<Entry> entries, int selectedTabId
) { |
| 102 super(DocumentActivity.class, IncognitoDocumentActivity.class); |
| 103 mEntries = entries; |
| 104 mSelectedTabId = selectedTabId; |
| 105 } |
| 106 |
| 107 public int getSelectedTabId() { |
| 108 return mSelectedTabId; |
| 109 } |
| 110 |
| 111 @Override |
| 112 public boolean isValidActivity(boolean isIncognito, Intent intent) { |
| 113 return true; |
| 114 } |
| 115 |
| 116 @Override |
| 117 public List<Entry> getTasksFromRecents(boolean isIncognito) { |
| 118 // We need to have our own list here, since these entries have not a
ctually been |
| 119 // created in Recents yet. |
| 120 return mEntries; |
| 121 } |
| 122 } |
| 123 |
| 124 private static class MigrationTabModel extends DocumentTabModelImpl { |
| 125 private final SparseArray<String> mTitleList; |
| 126 |
| 127 /** |
| 128 * Constucts a {@link DocumentTabModel} to be used for migration. |
| 129 * @param activityDelegate The delegate that has the tabs to be migrated
. |
| 130 * @param storageDelegate Delegate that interacts with the file system. |
| 131 */ |
| 132 MigrationTabModel(MigrationActivityDelegate activityDelegate, |
| 133 StorageDelegate storageDelegate) { |
| 134 super(activityDelegate, storageDelegate, new TabDelegateImpl(), fals
e, |
| 135 Tab.INVALID_TAB_ID, ApplicationStatus.getApplicationContext(
)); |
| 136 startTabStateLoad(); |
| 137 mTitleList = new SparseArray<String>(); |
| 138 setLastShownId(activityDelegate.getSelectedTabId()); |
| 139 } |
| 140 |
| 141 /** |
| 142 * Returns the display title for the Document with the given ID. |
| 143 * @param tabId The ID for the document to return the url for. |
| 144 * @return The display title for the entry if it was found, null otherwi
se. |
| 145 */ |
| 146 public String getTitleForDocument(int tabId) { |
| 147 String title = mTitleList.get(tabId); |
| 148 return TextUtils.isEmpty(title) ? "" : title; |
| 149 } |
| 150 |
| 151 @Override |
| 152 protected boolean shouldStartDeserialization(int currentState) { |
| 153 return currentState == STATE_LOAD_TAB_STATE_BG_END; |
| 154 } |
| 155 |
| 156 @Override |
| 157 protected void updateEntryInfoFromTabState(Entry entry, TabState tabStat
e) { |
| 158 super.updateEntryInfoFromTabState(entry, tabState); |
| 159 mTitleList.put(entry.tabId, tabState.getDisplayTitleFromState()); |
| 160 } |
| 161 } |
| 162 |
| 163 /** |
| 164 * Migrates all tab state to classic mode and creates a tab model file using
the current |
| 165 * {@link DocumentTabModel} instances. |
| 166 * @param activity The activity to be finished after migration if necessary. |
| 167 * @param finalizeMode The mode in which the migration should be finalized. |
| 168 */ |
| 169 public static void migrateTabsFromDocumentToClassic(final Activity activity, |
| 170 int finalizeMode) { |
| 171 Context context = ApplicationStatus.getApplicationContext(); |
| 172 // Before migration we remove all incognito tabs and also remove the |
| 173 // tabs that can not be reached through the {@link DocumentTabModel} ins
tances. |
| 174 List<Integer> tabIdsToRemove = new ArrayList<Integer>(); |
| 175 |
| 176 DocumentTabModelImpl normalTabModel = (DocumentTabModelImpl) |
| 177 ChromeMobileApplication.getDocumentTabModelSelector().getModel(f
alse); |
| 178 OffTheRecordDocumentTabModel incognitoTabModel = (OffTheRecordDocumentTa
bModel) |
| 179 ChromeMobileApplication.getDocumentTabModelSelector().getModel(t
rue); |
| 180 |
| 181 // TODO(yusufo): Clean up this logic. |
| 182 for (int i = 0; i < incognitoTabModel.getCount(); i++) { |
| 183 tabIdsToRemove.add(incognitoTabModel.getTabAt(i).getId()); |
| 184 } |
| 185 |
| 186 ActivityManager am = |
| 187 (ActivityManager) context.getSystemService(Activity.ACTIVITY_SER
VICE); |
| 188 List<AppTask> taskList = am.getAppTasks(); |
| 189 for (int i = 0; i < taskList.size(); i++) { |
| 190 Intent intent = DocumentUtils.getBaseIntentFromTask(taskList.get(i))
; |
| 191 int id = ActivityDelegate.getTabIdFromIntent(intent); |
| 192 if (id == Tab.INVALID_TAB_ID) continue; |
| 193 if (tabIdsToRemove.contains(id)) taskList.get(i).finishAndRemoveTask
(); |
| 194 } |
| 195 incognitoTabModel.updateRecentlyClosed(); |
| 196 |
| 197 File migratedFolder = TabPersistentStore.getStateDirectory(context, 0); |
| 198 String tabStatefileName = new File(migratedFolder, |
| 199 TabPersistentStore.SAVED_STATE_FILE).getAbsolutePath(); |
| 200 |
| 201 // All the TabStates (incognito or not) live in the same directory. |
| 202 File[] allTabs = normalTabModel.getStorageDelegate().getStateDirectory()
.listFiles(); |
| 203 try { |
| 204 for (int i = 0; i < allTabs.length; i++) { |
| 205 String fileName = allTabs[i].getName(); |
| 206 Pair<Integer, Boolean> tabInfo = TabState.parseInfoFromFilename(
fileName); |
| 207 if (tabInfo == null) continue; |
| 208 int tabId = tabInfo.first; |
| 209 |
| 210 // Also remove the tab state file for the closed tabs. |
| 211 boolean success; |
| 212 if (!tabIdsToRemove.contains(tabId)) { |
| 213 success = allTabs[i].renameTo(new File(migratedFolder, fileN
ame)); |
| 214 } else { |
| 215 success = allTabs[i].delete(); |
| 216 } |
| 217 |
| 218 if (!success) Log.e(TAG, "Failed to move/delete file for tab ID:
" + tabId); |
| 219 } |
| 220 |
| 221 if (normalTabModel.getCount() != 0) { |
| 222 byte[] listData; |
| 223 listData = TabPersistentStore.serializeTabLists(incognitoTabMode
l, normalTabModel); |
| 224 ImportantFileWriterAndroid.writeFileAtomically(tabStatefileName,
listData); |
| 225 } |
| 226 } catch (IOException e) { |
| 227 Log.e(TAG , "IO exception during tab migration, tab state might not
restore correctly"); |
| 228 } |
| 229 finalizeMigration(activity, finalizeMode); |
| 230 } |
| 231 |
| 232 /** |
| 233 * Migrates all tab state to document mode and creates tasks for each curren
tly open tab. |
| 234 * @param activity Activity to be used while launching the tasks. |
| 235 * @param finalizeMode The mode in which the migration should be finalized. |
| 236 */ |
| 237 public static void migrateTabsFromClassicToDocument( |
| 238 final Activity activity, final int finalizeMode) { |
| 239 StorageDelegate storageDelegate = new StorageDelegate(); |
| 240 MigrationActivityDelegate activityDelegate = |
| 241 createActivityDelegateWithTabsToMigrate(storageDelegate, activit
y); |
| 242 final MigrationTabModel normalTabModel = |
| 243 new MigrationTabModel(activityDelegate, storageDelegate); |
| 244 |
| 245 InitializationObserver observer = new InitializationObserver(normalTabMo
del) { |
| 246 @Override |
| 247 protected void runImmediately() { |
| 248 addAppTasksFromFiles(activity, normalTabModel, finalizeMode); |
| 249 } |
| 250 |
| 251 @Override |
| 252 public boolean isSatisfied(int currentState) { |
| 253 return currentState == DocumentTabModelImpl.STATE_DESERIALIZE_EN
D; |
| 254 } |
| 255 |
| 256 @Override |
| 257 public boolean isCanceled() { |
| 258 return false; |
| 259 } |
| 260 }; |
| 261 |
| 262 observer.runWhenReady(); |
| 263 } |
| 264 |
| 265 /** |
| 266 * Migrate tabs saved in classic mode to document mode for an upgrade. This
doesn't restart |
| 267 * the app process but only finishes the {@link ChromeLauncherActivity} it w
as being called |
| 268 * with. |
| 269 * @param activity The activity to use for carrying out and finalizing the m
igration. |
| 270 * @param finalizeMode The mode in which the migration should be finalized. |
| 271 * @return Whether any tabs will be migrated. |
| 272 */ |
| 273 public static boolean migrateTabsToDocumentForUpgrade(Activity activity, |
| 274 int finalizeMode) { |
| 275 ChromePreferenceManager.getInstance(activity).setAttemptedMigrationOnUpg
rade(); |
| 276 File[] fileList = TabPersistentStore.getStateDirectory(activity, 0).list
Files(); |
| 277 if (fileList == null || fileList.length == 0 |
| 278 || (fileList.length == 1 |
| 279 && fileList[0].getName().equals(TabPersistentStore.SAVED_STATE_F
ILE))) { |
| 280 return false; |
| 281 } |
| 282 |
| 283 migrateTabsFromClassicToDocument(activity, finalizeMode); |
| 284 return true; |
| 285 } |
| 286 |
| 287 private static void finalizeMigration(Activity activity, final int mode) { |
| 288 switch(mode) { |
| 289 case FINALIZE_MODE_NO_ACTION: |
| 290 return; |
| 291 case FINALIZE_MODE_FINISH_ACTIVITY: |
| 292 activity.finishAndRemoveTask(); |
| 293 return; |
| 294 case FINALIZE_MODE_RESTART_APP: |
| 295 ApplicationLifetime.terminate(true); |
| 296 return; |
| 297 default: |
| 298 assert false; |
| 299 } |
| 300 } |
| 301 |
| 302 private static MigrationActivityDelegate createActivityDelegateWithTabsToMig
rate( |
| 303 StorageDelegate storageDelegate, Activity activity) { |
| 304 File migratedFolder = storageDelegate.getStateDirectory(); |
| 305 if (!migratedFolder.exists() && !migratedFolder.mkdir()) { |
| 306 Log.e(TAG, "Failed to create folder: " + migratedFolder.getAbsoluteP
ath()); |
| 307 } |
| 308 |
| 309 // Create maps for all tabs that will be used during TabModel initializa
tion. |
| 310 final List<Entry> normalEntryMap = new ArrayList<Entry>(); |
| 311 |
| 312 int currentSelectorIndex = 0; |
| 313 File currentFolder = TabPersistentStore.getStateDirectory(activity, curr
entSelectorIndex); |
| 314 MigrationTabStateReadCallback callback = new MigrationTabStateReadCallba
ck(); |
| 315 while (currentFolder.listFiles() != null && currentFolder.listFiles().le
ngth != 0) { |
| 316 File[] allTabs = TabPersistentStore |
| 317 .getStateDirectory(activity, currentSelectorIndex).listFiles
(); |
| 318 try { |
| 319 TabPersistentStore.readSavedStateFile(currentFolder, callback); |
| 320 } catch (IOException e) { |
| 321 Log.e(TAG, "IO Exception while trying to get the last used tab i
d"); |
| 322 } |
| 323 |
| 324 for (int i = 0; i < allTabs.length; i++) { |
| 325 // Move tab state file to the document side folder. |
| 326 String fileName = allTabs[i].getName(); |
| 327 Pair<Integer, Boolean> tabInfo = TabState.parseInfoFromFilename(
fileName); |
| 328 if (tabInfo == null) continue; |
| 329 |
| 330 boolean success; |
| 331 if (tabInfo.second) { |
| 332 success = allTabs[i].delete(); |
| 333 } else { |
| 334 success = allTabs[i].renameTo(new File(migratedFolder, fileN
ame)); |
| 335 normalEntryMap.add(new Entry(tabInfo.first, UrlConstants.NTP
_URL)); |
| 336 } |
| 337 |
| 338 if (!success) Log.e(TAG, "Failed to move/delete file: " + fileNa
me); |
| 339 } |
| 340 currentSelectorIndex++; |
| 341 currentFolder = TabPersistentStore.getStateDirectory(activity, curre
ntSelectorIndex); |
| 342 } |
| 343 |
| 344 return new MigrationActivityDelegate(normalEntryMap, callback.getSelecte
dTabId()); |
| 345 } |
| 346 |
| 347 private static void addAppTasksFromFiles(final Activity activity, |
| 348 final MigrationTabModel tabModel, final int finalizeMode) { |
| 349 if (tabModel.getCount() == 0) { |
| 350 finalizeMigration(activity, finalizeMode); |
| 351 return; |
| 352 } |
| 353 |
| 354 final TabContentManager contentManager = |
| 355 new TabContentManager(activity, new ContentOffsetProvider() { |
| 356 @Override |
| 357 public int getOverlayTranslateY() { |
| 358 return 0; |
| 359 } |
| 360 }, DeviceClassManager.enableSnapshots()); |
| 361 FaviconHelper faviconHelper = new FaviconHelper(); |
| 362 for (int i = 0; i < tabModel.getCount(); i++) { |
| 363 final int tabId = tabModel.getTabAt(i).getId(); |
| 364 String currentUrl = tabModel.getCurrentUrlForDocument(tabId); |
| 365 String currentTitle = tabModel.getTitleForDocument(tabId); |
| 366 final boolean finalizeWhenDone = i == tabModel.getCount() - 1; |
| 367 |
| 368 // Use placeholders if we can't find anything for url and title. |
| 369 if (TextUtils.isEmpty(currentUrl)) currentUrl = UrlConstants.NTP_URL
; |
| 370 if (TextUtils.isEmpty(currentTitle) |
| 371 && !NativePageFactory.isNativePageUrl(currentUrl, false)) { |
| 372 currentTitle = UrlUtilities.getDomainAndRegistry(currentUrl, fal
se); |
| 373 } |
| 374 final String url = currentUrl; |
| 375 final String title = currentTitle; |
| 376 |
| 377 faviconHelper.getLargestRawFaviconForUrl( |
| 378 Profile.getLastUsedProfile().getOriginalProfile(), |
| 379 url, ICON_TYPES, DESIRED_ICON_SIZE_DP, |
| 380 new FaviconImageCallback() { |
| 381 @Override |
| 382 public void onFaviconAvailable(final Bitmap favicon, Str
ing iconUrl) { |
| 383 // Even if either the favicon or the thumbnail comes
back null |
| 384 // add the AppTask with the return values. The frame
work handles a null |
| 385 // favicon and addAppTask below handles null thumbna
ils. |
| 386 DecompressThumbnailCallback thumbnailCallback = |
| 387 new DecompressThumbnailCallback() { |
| 388 @Override |
| 389 public void onFinishGetBitmap(Bitmap bitmap) { |
| 390 if (!NativePageFactory.isNativePageUrl(url,
false) |
| 391 && !url.startsWith(UrlConstants.CHRO
ME_SCHEME)) { |
| 392 addAppTask(activity, tabId, |
| 393 tabModel.getTabStateForDocument(
tabId), |
| 394 url, title, favicon, bitmap); |
| 395 } |
| 396 // TODO(yusufo) : Have a counter here to mak
e sure all tabs |
| 397 // before this one has been a
dded. |
| 398 if (finalizeWhenDone) { |
| 399 finalizeMigration(activity, finalizeMode
); |
| 400 } |
| 401 } |
| 402 }; |
| 403 contentManager.getThumbnailForId(tabId, thumbnailCal
lback); |
| 404 } |
| 405 }); |
| 406 } |
| 407 } |
| 408 |
| 409 private static void addAppTask(Activity activity, int tabId, TabState tabSta
te, |
| 410 String currentUrl, String title, Bitmap favicon, Bitmap bitmap) { |
| 411 if (tabId == ActivityDelegate.getTabIdFromIntent(activity.getIntent()))
return; |
| 412 // Create intent and taskDescription. |
| 413 Intent intent = new Intent(Intent.ACTION_VIEW, |
| 414 DocumentTabModelSelector.createDocumentDataString(tabId, current
Url)); |
| 415 intent.setClassName(activity, ChromeLauncherActivity.getDocumentClassNam
e(false)); |
| 416 intent.putExtra(IntentHandler.EXTRA_PRESERVE_TASK, true); |
| 417 ActivityManager am = |
| 418 (ActivityManager) activity.getSystemService(Activity.ACTIVITY_SE
RVICE); |
| 419 |
| 420 Bitmap thumbnail = Bitmap.createBitmap(am.getAppTaskThumbnailSize().getW
idth(), |
| 421 am.getAppTaskThumbnailSize().getHeight(), Config.ARGB_8888); |
| 422 Canvas canvas = new Canvas(thumbnail); |
| 423 if (bitmap == null) { |
| 424 canvas.drawColor(Color.WHITE); |
| 425 } else { |
| 426 float scale = Math.max( |
| 427 (float) thumbnail.getWidth() / bitmap.getWidth(), |
| 428 (float) thumbnail.getHeight() / bitmap.getHeight()); |
| 429 canvas.scale(scale, scale); |
| 430 canvas.drawBitmap(bitmap, 0, 0, null); |
| 431 } |
| 432 TaskDescription taskDescription = new TaskDescription(title, favicon, |
| 433 activity.getResources().getColor(R.color.default_primary_color))
; |
| 434 am.addAppTask(activity, intent, taskDescription, thumbnail); |
| 435 Entry entry = new Entry(tabId, tabState); |
| 436 DocumentTabModelImpl tabModel = (DocumentTabModelImpl) ChromeMobileAppli
cation |
| 437 .getDocumentTabModelSelector().getModel(false); |
| 438 tabModel.addEntryForMigration(entry); |
| 439 } |
| 440 |
| 441 |
| 442 /** |
| 443 * Migrates tabs with state to and from document mode. |
| 444 * @param toDocumentMode Whether the user is opting out. If true the migrati
on is from Document |
| 445 * to Classic mode. |
| 446 * @param activity The activity to use for launching intent if needed. |
| 447 * @param terminate Whether the application process should be terminated aft
er the migration. |
| 448 */ |
| 449 public static void migrateTabs(boolean toDocumentMode, final Activity activi
ty, |
| 450 boolean terminate) { |
| 451 int terminateMode = terminate ? FINALIZE_MODE_RESTART_APP : FINALIZE_MOD
E_FINISH_ACTIVITY; |
| 452 if (toDocumentMode) { |
| 453 migrateTabsFromClassicToDocument(activity, terminateMode); |
| 454 } else { |
| 455 migrateTabsFromDocumentToClassic(activity, terminateMode); |
| 456 } |
| 457 } |
| 458 } |
OLD | NEW |