1 // Copyright 2019 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.tasks.tab_groups; 6 7 import android.content.Context; 8 import android.content.SharedPreferences; 9 10 import androidx.annotation.NonNull; 11 import androidx.annotation.VisibleForTesting; 12 13 import org.chromium.base.ContextUtils; 14 import org.chromium.base.MathUtils; 15 import org.chromium.base.ObserverList; 16 import org.chromium.base.ThreadUtils; 17 import org.chromium.base.metrics.RecordHistogram; 18 import org.chromium.base.metrics.RecordUserAction; 19 import org.chromium.base.task.AsyncTask; 20 import org.chromium.chrome.browser.tab.Tab; 21 import org.chromium.chrome.browser.tab.TabLaunchType; 22 import org.chromium.chrome.browser.tab.state.CriticalPersistedTabData; 23 import org.chromium.chrome.browser.tabmodel.TabList; 24 import org.chromium.chrome.browser.tabmodel.TabModel; 25 import org.chromium.chrome.browser.tabmodel.TabModelFilter; 26 import org.chromium.chrome.browser.tabmodel.TabModelUtils; 27 28 import java.util.ArrayList; 29 import java.util.Collections; 30 import java.util.HashMap; 31 import java.util.LinkedHashSet; 32 import java.util.List; 33 import java.util.Map; 34 import java.util.Set; 35 36 /** 37 * An implementation of {@link TabModelFilter} that puts {@link Tab}s into a group 38 * structure. 39 * 40 * A group is a collection of {@link Tab}s that share a common ancestor {@link Tab}. This filter is 41 * also a {@link TabList} that contains the last shown {@link Tab} from every group. 42 */ 43 public class TabGroupModelFilter extends TabModelFilter { 44 private static final String PREFS_FILE = "tab_group_pref"; 45 private static final String SESSIONS_COUNT_FOR_GROUP = "SessionsCountForGroup-"; 46 private static SharedPreferences sPref; 47 48 /** 49 * An interface to be notified about changes to a {@link TabGroupModelFilter}. 50 */ 51 public interface Observer { 52 /** 53 * This method is called before a tab is moved to form a group or moved into an existed 54 * group. 55 * @param movedTab The {@link Tab} which will be moved. If a group will be merged to a tab 56 * or another group, this is the last tab of the merged group. 57 * @param newRootId The new root id of the group after merge. 58 */ willMergeTabToGroup(Tab movedTab, int newRootId)59 void willMergeTabToGroup(Tab movedTab, int newRootId); 60 61 /** 62 * This method is called before a tab within a group is moved out of the group. 63 * 64 * @param movedTab The tab which will be moved. 65 * @param newRootId The new root id of the group from which {@code movedTab} is moved out. 66 */ willMoveTabOutOfGroup(Tab movedTab, int newRootId)67 void willMoveTabOutOfGroup(Tab movedTab, int newRootId); 68 69 /** 70 * This method is called after a tab is moved to form a group or moved into an existed 71 * group. 72 * @param movedTab The {@link Tab} which has been moved. If a group is merged to a tab or 73 * another group, this is the last tab of the merged group. 74 * @param selectedTabIdInGroup The id of the selected {@link Tab} in group. 75 */ didMergeTabToGroup(Tab movedTab, int selectedTabIdInGroup)76 void didMergeTabToGroup(Tab movedTab, int selectedTabIdInGroup); 77 78 /** 79 * This method is called after a group is moved. 80 * 81 * @param movedTab The tab which has been moved. This is the last tab within the group. 82 * @param tabModelOldIndex The old index of the {@code movedTab} in the {@link TabModel}. 83 * @param tabModelNewIndex The new index of the {@code movedTab} in the {@link TabModel}. 84 */ didMoveTabGroup(Tab movedTab, int tabModelOldIndex, int tabModelNewIndex)85 void didMoveTabGroup(Tab movedTab, int tabModelOldIndex, int tabModelNewIndex); 86 87 /** 88 * This method is called after a tab within a group is moved. 89 * 90 * @param movedTab The tab which has been moved. 91 * @param tabModelOldIndex The old index of the {@code movedTab} in the {@link TabModel}. 92 * @param tabModelNewIndex The new index of the {@code movedTab} in the {@link TabModel}. 93 */ didMoveWithinGroup(Tab movedTab, int tabModelOldIndex, int tabModelNewIndex)94 void didMoveWithinGroup(Tab movedTab, int tabModelOldIndex, int tabModelNewIndex); 95 96 /** 97 * This method is called after a tab within a group is moved out of the group. 98 * 99 * @param movedTab The tab which has been moved. 100 * @param prevFilterIndex The index in {@link TabGroupModelFilter} of the group where {@code 101 * moveTab} is in before ungrouping. 102 */ didMoveTabOutOfGroup(Tab movedTab, int prevFilterIndex)103 void didMoveTabOutOfGroup(Tab movedTab, int prevFilterIndex); 104 105 /** 106 * This method is called after a group is created manually by user. Either using the 107 * TabSelectionEditor (Group tab menu item) or using drag and drop. 108 * @param tabs The list of modified {@link Tab}s. 109 * @param tabOriginalIndex The original tab index for each modified tab. 110 * @param isSameGroup Whether the given list is in a group already. 111 */ didCreateGroup( List<Tab> tabs, List<Integer> tabOriginalIndex, boolean isSameGroup)112 void didCreateGroup( 113 List<Tab> tabs, List<Integer> tabOriginalIndex, boolean isSameGroup); 114 } 115 116 /** 117 * This class is a representation of a group of tabs. It knows the last selected tab within the 118 * group. 119 */ 120 private class TabGroup { 121 private static final int INVALID_GROUP_ID = -1; 122 private final Set<Integer> mTabIds; 123 private int mLastShownTabId; 124 private int mGroupId; 125 TabGroup(int groupId)126 TabGroup(int groupId) { 127 mTabIds = new LinkedHashSet<>(); 128 mLastShownTabId = Tab.INVALID_TAB_ID; 129 mGroupId = groupId; 130 } 131 addTab(int tabId)132 void addTab(int tabId) { 133 mTabIds.add(tabId); 134 if (mLastShownTabId == Tab.INVALID_TAB_ID) setLastShownTabId(tabId); 135 if (size() > 1) reorderGroup(mGroupId); 136 } 137 removeTab(int tabId)138 void removeTab(int tabId) { 139 assert mTabIds.contains(tabId); 140 if (mLastShownTabId == tabId) { 141 int nextIdToShow = nextTabIdToShow(tabId); 142 if (nextIdToShow != Tab.INVALID_TAB_ID) setLastShownTabId(nextIdToShow); 143 } 144 mTabIds.remove(tabId); 145 } 146 moveToEndInGroup(int tabId)147 void moveToEndInGroup(int tabId) { 148 if (!mTabIds.contains(tabId)) return; 149 mTabIds.remove(tabId); 150 mTabIds.add(tabId); 151 } 152 contains(int tabId)153 boolean contains(int tabId) { 154 return mTabIds.contains(tabId); 155 } 156 size()157 int size() { 158 return mTabIds.size(); 159 } 160 getTabIdList()161 List<Integer> getTabIdList() { 162 return Collections.unmodifiableList(new ArrayList<>(mTabIds)); 163 } 164 getLastShownTabId()165 int getLastShownTabId() { 166 return mLastShownTabId; 167 } 168 setLastShownTabId(int tabId)169 void setLastShownTabId(int tabId) { 170 assert mTabIds.contains(tabId); 171 mLastShownTabId = tabId; 172 } 173 nextTabIdToShow(int tabId)174 int nextTabIdToShow(int tabId) { 175 if (mTabIds.size() == 1 || !mTabIds.contains(tabId)) return Tab.INVALID_TAB_ID; 176 List<Integer> ids = getTabIdList(); 177 int position = ids.indexOf(tabId); 178 if (position == 0) return ids.get(position + 1); 179 return ids.get(position - 1); 180 } 181 getTabIdForIndex(int index)182 int getTabIdForIndex(int index) { 183 return getTabIdList().get(index); 184 } 185 } 186 private ObserverList<Observer> mGroupFilterObserver = new ObserverList<>(); 187 private Map<Integer, Integer> mGroupIdToGroupIndexMap = new HashMap<>(); 188 private Map<Integer, TabGroup> mGroupIdToGroupMap = new HashMap<>(); 189 private int mCurrentGroupIndex = TabList.INVALID_TAB_INDEX; 190 // The number of groups with at least 2 tabs. 191 private int mActualGroupCount; 192 private Tab mAbsentSelectedTab; 193 private boolean mShouldRecordUma = true; 194 private boolean mIsResetting; 195 TabGroupModelFilter(TabModel tabModel)196 public TabGroupModelFilter(TabModel tabModel) { 197 super(tabModel); 198 } 199 200 /** 201 * This method adds a {@link Observer} to be notified on {@link TabGroupModelFilter} changes. 202 * @param observer The {@link Observer} to add. 203 */ addTabGroupObserver(Observer observer)204 public void addTabGroupObserver(Observer observer) { 205 mGroupFilterObserver.addObserver(observer); 206 } 207 208 /** 209 * This method removes a {@link Observer}. 210 * @param observer The {@link Observer} to remove. 211 */ removeTabGroupObserver(Observer observer)212 public void removeTabGroupObserver(Observer observer) { 213 mGroupFilterObserver.removeObserver(observer); 214 } 215 216 /** 217 * @return Number of {@link TabGroup}s that has at least two tabs. 218 */ getTabGroupCount()219 public int getTabGroupCount() { 220 return mActualGroupCount; 221 } 222 223 /** 224 * This method records the number of sessions of the provided {@link Tab}, only if that 225 * {@link Tab} is in a group that has at least two tab, and it records as 226 * "TabGroups.SessionPerGroup". 227 * @param tab {@link Tab} 228 */ recordSessionsCount(Tab tab)229 public void recordSessionsCount(Tab tab) { 230 int groupId = getRootId(tab); 231 boolean isActualGroup = mGroupIdToGroupMap.get(groupId) != null 232 && mGroupIdToGroupMap.get(groupId).size() > 1; 233 if (!isActualGroup) return; 234 235 AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> { 236 int sessionsCount = updateAndGetSessionsCount(groupId); 237 RecordHistogram.recordCountHistogram("TabGroups.SessionsPerGroup", sessionsCount); 238 }); 239 } 240 241 /** 242 * This method moves the TabGroup which contains the Tab with TabId {@code id} to 243 * {@code newIndex} in TabModel. 244 * @param id The id of the tab whose related tabs are being moved. 245 * @param newIndex The new index in TabModel that these tabs are being moved to. 246 */ moveRelatedTabs(int id, int newIndex)247 public void moveRelatedTabs(int id, int newIndex) { 248 List<Tab> tabs = getRelatedTabList(id); 249 TabModel tabModel = getTabModel(); 250 newIndex = MathUtils.clamp(newIndex, 0, tabModel.getCount()); 251 int curIndex = TabModelUtils.getTabIndexById(tabModel, tabs.get(0).getId()); 252 253 if (curIndex == INVALID_TAB_INDEX || curIndex == newIndex) { 254 return; 255 } 256 257 int offset = 0; 258 for (Tab tab : tabs) { 259 if (tabModel.indexOf(tab) == -1) { 260 assert false : "Tried to close a tab from another model!"; 261 continue; 262 } 263 tabModel.moveTab(tab.getId(), newIndex >= curIndex ? newIndex : newIndex + offset++); 264 } 265 } 266 267 /** 268 * This method merges the source group that contains the {@code sourceTabId} to the destination 269 * group that contains the {@code destinationTabId}. This method only operates if two groups are 270 * in the same {@code TabModel}. 271 * 272 * @param sourceTabId The id of the {@link Tab} to get the source group. 273 * @param destinationTabId The id of a {@link Tab} to get the destination group. 274 */ mergeTabsToGroup(int sourceTabId, int destinationTabId)275 public void mergeTabsToGroup(int sourceTabId, int destinationTabId) { 276 Tab sourceTab = TabModelUtils.getTabById(getTabModel(), sourceTabId); 277 Tab destinationTab = TabModelUtils.getTabById(getTabModel(), destinationTabId); 278 279 assert sourceTab != null && destinationTab != null 280 && sourceTab.isIncognito() 281 == destinationTab.isIncognito() 282 : "Attempting to merge groups from different model"; 283 284 int destinationGroupId = getRootId(destinationTab); 285 List<Tab> tabsToMerge = getRelatedTabList(sourceTabId); 286 int destinationIndexInTabModel = getTabModelDestinationIndex(destinationTab); 287 288 if (!needToUpdateTabModel(tabsToMerge, destinationIndexInTabModel)) { 289 for (Observer observer : mGroupFilterObserver) { 290 observer.willMergeTabToGroup( 291 tabsToMerge.get(tabsToMerge.size() - 1), destinationGroupId); 292 } 293 for (int i = 0; i < tabsToMerge.size(); i++) { 294 setRootId(tabsToMerge.get(i), destinationGroupId); 295 } 296 resetFilterState(); 297 298 Tab lastMergedTab = tabsToMerge.get(tabsToMerge.size() - 1); 299 TabGroup group = mGroupIdToGroupMap.get(getRootId(lastMergedTab)); 300 for (Observer observer : mGroupFilterObserver) { 301 observer.didMergeTabToGroup( 302 tabsToMerge.get(tabsToMerge.size() - 1), group.getLastShownTabId()); 303 } 304 } else { 305 mergeListOfTabsToGroup(tabsToMerge, destinationTab, true, false); 306 } 307 // TODO(978508): Send didCreateGroup signal to activate the 308 // {@link UndoGroupSnackbarController}. 309 } 310 311 /** 312 * This method appends a list of {@link Tab}s to the destination group that contains the 313 * {@code} destinationTab. The {@link TabModel} ordering of the tabs in the given list is not 314 * preserved. After calling this method, the {@link TabModel} ordering of these tabs would 315 * become the ordering of {@code tabs}. 316 * 317 * @param tabs List of {@link Tab}s to be appended. 318 * @param destinationTab The destination {@link Tab} to be append to. 319 * @param isSameGroup Whether the given list of {@link Tab}s belongs in the same group 320 * originally. 321 * @param notify Whether or not to notify observers about the merging events. 322 */ mergeListOfTabsToGroup( List<Tab> tabs, Tab destinationTab, boolean isSameGroup, boolean notify)323 public void mergeListOfTabsToGroup( 324 List<Tab> tabs, Tab destinationTab, boolean isSameGroup, boolean notify) { 325 int destinationGroupId = getRootId(destinationTab); 326 int destinationIndexInTabModel = getTabModelDestinationIndex(destinationTab); 327 List<Integer> originalIndexes = new ArrayList<>(); 328 329 for (int i = 0; i < tabs.size(); i++) { 330 Tab tab = tabs.get(i); 331 // When merging tabs are in the same group, only make one willMergeTabToGroup call. 332 if (!isSameGroup || i == tabs.size() - 1) { 333 for (Observer observer : mGroupFilterObserver) { 334 observer.willMergeTabToGroup(tab, destinationGroupId); 335 } 336 } 337 int index = TabModelUtils.getTabIndexById(getTabModel(), tab.getId()); 338 assert index != TabModel.INVALID_TAB_INDEX; 339 originalIndexes.add(index); 340 341 if (tab.getId() == destinationTab.getId()) continue; 342 343 boolean isMergingBackward = index < destinationIndexInTabModel; 344 345 setRootId(tab, destinationGroupId); 346 getTabModel().moveTab(tab.getId(), 347 isMergingBackward ? destinationIndexInTabModel : destinationIndexInTabModel++); 348 } 349 350 if (notify) { 351 for (Observer observer : mGroupFilterObserver) { 352 observer.didCreateGroup(tabs, originalIndexes, isSameGroup); 353 } 354 } 355 } 356 357 /** 358 * This method moves Tab with id as {@code sourceTabId} out of the group it belongs to. 359 * 360 * @param sourceTabId The id of the {@link Tab} to get the source group. 361 */ 362 public void moveTabOutOfGroup(int sourceTabId) { 363 TabModel tabModel = getTabModel(); 364 Tab sourceTab = TabModelUtils.getTabById(tabModel, sourceTabId); 365 int sourceIndex = tabModel.indexOf(sourceTab); 366 TabGroup sourceTabGroup = mGroupIdToGroupMap.get(getRootId(sourceTab)); 367 Tab lastTabInSourceGroup = TabModelUtils.getTabById(tabModel, 368 sourceTabGroup.getTabIdForIndex(sourceTabGroup.getTabIdList().size() - 1)); 369 int targetIndex = tabModel.indexOf(lastTabInSourceGroup); 370 assert targetIndex != TabModel.INVALID_TAB_INDEX; 371 372 int prevFilterIndex = mGroupIdToGroupIndexMap.get(getRootId(sourceTab)); 373 if (sourceTabGroup.size() == 1) { 374 for (Observer observer : mGroupFilterObserver) { 375 observer.didMoveTabOutOfGroup(sourceTab, prevFilterIndex); 376 } 377 return; 378 } 379 int newRootId = getRootId(sourceTab); 380 if (sourceTab.getId() == getRootId(sourceTab)) { 381 // If moving tab's id is the root id of the group, find a new root id. 382 if (sourceIndex != 0 383 && getRootId(tabModel.getTabAt(sourceIndex - 1)) == getRootId(sourceTab)) { 384 newRootId = tabModel.getTabAt(sourceIndex - 1).getId(); 385 } else if (sourceIndex != tabModel.getCount() - 1 386 && getRootId(tabModel.getTabAt(sourceIndex + 1)) == getRootId(sourceTab)) { 387 newRootId = tabModel.getTabAt(sourceIndex + 1).getId(); 388 } 389 } 390 assert newRootId != Tab.INVALID_TAB_ID; 391 392 for (Observer observer : mGroupFilterObserver) { 393 observer.willMoveTabOutOfGroup(sourceTab, newRootId); 394 } 395 if (sourceTab.getId() == getRootId(sourceTab)) { 396 for (int tabId : sourceTabGroup.getTabIdList()) { 397 setRootId(TabModelUtils.getTabById(tabModel, tabId), newRootId); 398 } 399 resetFilterState(); 400 } 401 setRootId(sourceTab, sourceTab.getId()); 402 // If moving tab is already in the target index in tab model, no move in tab model. 403 if (sourceIndex == targetIndex) { 404 resetFilterState(); 405 for (Observer observer : mGroupFilterObserver) { 406 observer.didMoveTabOutOfGroup(sourceTab, prevFilterIndex); 407 } 408 return; 409 } 410 // Plus one as offset because we are moving backwards in tab model. 411 tabModel.moveTab(sourceTab.getId(), targetIndex + 1); 412 } 413 414 private int getTabModelDestinationIndex(Tab destinationTab) { 415 List<Integer> destinationGroupedTabIds = 416 mGroupIdToGroupMap.get(getRootId(destinationTab)).getTabIdList(); 417 int destinationTabIndex = TabModelUtils.getTabIndexById( 418 getTabModel(), destinationGroupedTabIds.get(destinationGroupedTabIds.size() - 1)); 419 420 return destinationTabIndex + 1; 421 } 422 423 private boolean needToUpdateTabModel(List<Tab> tabsToMerge, int destinationIndexInTabModel) { 424 assert tabsToMerge.size() > 0; 425 426 int firstTabIndexInTabModel = 427 TabModelUtils.getTabIndexById(getTabModel(), tabsToMerge.get(0).getId()); 428 return firstTabIndexInTabModel != destinationIndexInTabModel; 429 } 430 431 /** 432 * This method undo the given grouped {@link Tab}. 433 * 434 * @param tab undo this grouped {@link Tab}. 435 * @param originalIndex The tab index before grouped. 436 * @param originalGroupId The rootId before grouped. 437 */ undoGroupedTab(Tab tab, int originalIndex, int originalGroupId)438 public void undoGroupedTab(Tab tab, int originalIndex, int originalGroupId) { 439 if (!tab.isInitialized()) { 440 return; 441 } 442 int currentIndex = TabModelUtils.getTabIndexById(getTabModel(), tab.getId()); 443 assert currentIndex != TabModel.INVALID_TAB_INDEX; 444 445 setRootId(tab, originalGroupId); 446 if (currentIndex == originalIndex) { 447 didMoveTab(tab, originalIndex, currentIndex); 448 } else { 449 if (currentIndex < originalIndex) originalIndex++; 450 getTabModel().moveTab(tab.getId(), originalIndex); 451 } 452 } 453 454 // TODO(crbug.com/951608): follow up with sessions count histogram for TabGroups. updateAndGetSessionsCount(int groupId)455 private int updateAndGetSessionsCount(int groupId) { 456 ThreadUtils.assertOnBackgroundThread(); 457 458 String sessionsCountForGroupKey = SESSIONS_COUNT_FOR_GROUP + Integer.toString(groupId); 459 SharedPreferences prefs = getSharedPreferences(); 460 int sessionsCount = prefs.getInt(sessionsCountForGroupKey, 0); 461 sessionsCount++; 462 prefs.edit().putInt(sessionsCountForGroupKey, sessionsCount).apply(); 463 return sessionsCount; 464 } 465 getSharedPreferences()466 private SharedPreferences getSharedPreferences() { 467 if (sPref == null) { 468 sPref = ContextUtils.getApplicationContext().getSharedPreferences( 469 PREFS_FILE, Context.MODE_PRIVATE); 470 } 471 return sPref; 472 } 473 474 // TabModelFilter implementation. 475 @NonNull 476 @Override getRelatedTabList(int id)477 public List<Tab> getRelatedTabList(int id) { 478 // TODO(meiliang): In worst case, this method runs in O(n^2). This method needs to perform 479 // better, especially when we try to call it in a loop for all tabs. 480 Tab tab = TabModelUtils.getTabById(getTabModel(), id); 481 if (tab == null) return super.getRelatedTabList(id); 482 483 int groupId = getRootId(tab); 484 TabGroup group = mGroupIdToGroupMap.get(groupId); 485 if (group == null) return super.getRelatedTabList(TabModel.INVALID_TAB_INDEX); 486 return getRelatedTabList(group.getTabIdList()); 487 } 488 489 /** 490 * This method returns all tabs in a tab group with reference to {@code tabRootId} as group id. 491 * 492 * @param tabRootId The tab root id that is used to find the related group. 493 * @return An unmodifiable list of {@link Tab} that relate with the given tab root id. 494 */ getRelatedTabListForRootId(int tabRootId)495 public List<Tab> getRelatedTabListForRootId(int tabRootId) { 496 if (tabRootId == Tab.INVALID_TAB_ID) return super.getRelatedTabList(tabRootId); 497 TabGroup group = mGroupIdToGroupMap.get(tabRootId); 498 if (group == null) return super.getRelatedTabList(TabModel.INVALID_TAB_INDEX); 499 return getRelatedTabList(group.getTabIdList()); 500 } 501 502 @Override hasOtherRelatedTabs(Tab tab)503 public boolean hasOtherRelatedTabs(Tab tab) { 504 int groupId = getRootId(tab); 505 TabGroup group = mGroupIdToGroupMap.get(groupId); 506 return group != null && group.size() > 1; 507 } 508 getRelatedTabList(List<Integer> ids)509 private List<Tab> getRelatedTabList(List<Integer> ids) { 510 List<Tab> tabs = new ArrayList<>(); 511 for (Integer id : ids) { 512 tabs.add(TabModelUtils.getTabById(getTabModel(), id)); 513 } 514 return Collections.unmodifiableList(tabs); 515 } 516 517 @Override addTab(Tab tab)518 protected void addTab(Tab tab) { 519 if (tab.isIncognito() != isIncognito()) { 520 throw new IllegalStateException("Attempting to open tab in the wrong model"); 521 } 522 523 if (isTabModelRestored() && !mIsResetting) { 524 Tab parentTab = TabModelUtils.getTabById( 525 getTabModel(), CriticalPersistedTabData.from(tab).getParentId()); 526 if (parentTab != null) { 527 setRootId(tab, getRootId(parentTab)); 528 } 529 } 530 531 int groupId = getRootId(tab); 532 if (mGroupIdToGroupMap.containsKey(groupId)) { 533 if (mGroupIdToGroupMap.get(groupId).size() == 1) { 534 mActualGroupCount++; 535 if (mShouldRecordUma 536 && tab.getLaunchType() == TabLaunchType.FROM_LONGPRESS_BACKGROUND) { 537 RecordUserAction.record("TabGroup.Created.OpenInNewTab"); 538 } 539 } 540 mGroupIdToGroupMap.get(groupId).addTab(tab.getId()); 541 } else { 542 TabGroup tabGroup = new TabGroup(getRootId(tab)); 543 tabGroup.addTab(tab.getId()); 544 mGroupIdToGroupMap.put(groupId, tabGroup); 545 mGroupIdToGroupIndexMap.put(groupId, mGroupIdToGroupIndexMap.size()); 546 } 547 548 if (mAbsentSelectedTab != null) { 549 Tab absentSelectedTab = mAbsentSelectedTab; 550 mAbsentSelectedTab = null; 551 selectTab(absentSelectedTab); 552 } 553 } 554 555 @Override closeTab(Tab tab)556 protected void closeTab(Tab tab) { 557 int groupId = getRootId(tab); 558 if (tab.isIncognito() != isIncognito() || mGroupIdToGroupMap.get(groupId) == null 559 || !mGroupIdToGroupMap.get(groupId).contains(tab.getId())) { 560 throw new IllegalStateException("Attempting to close tab in the wrong model"); 561 } 562 563 TabGroup group = mGroupIdToGroupMap.get(groupId); 564 group.removeTab(tab.getId()); 565 if (group.size() == 1) mActualGroupCount--; 566 if (group.size() == 0) { 567 updateGroupIdToGroupIndexMapAfterGroupClosed(groupId); 568 mGroupIdToGroupIndexMap.remove(groupId); 569 mGroupIdToGroupMap.remove(groupId); 570 AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> removeGroupFromPref(groupId)); 571 } 572 } 573 removeGroupFromPref(int groupId)574 private void removeGroupFromPref(int groupId) { 575 ThreadUtils.assertOnBackgroundThread(); 576 577 SharedPreferences prefs = getSharedPreferences(); 578 String key = SESSIONS_COUNT_FOR_GROUP + Integer.toString(groupId); 579 if (prefs.contains(key)) { 580 prefs.edit().remove(key).apply(); 581 } 582 } 583 updateGroupIdToGroupIndexMapAfterGroupClosed(int groupId)584 private void updateGroupIdToGroupIndexMapAfterGroupClosed(int groupId) { 585 int indexToRemove = mGroupIdToGroupIndexMap.get(groupId); 586 Set<Integer> groupIdSet = mGroupIdToGroupIndexMap.keySet(); 587 for (Integer groupIdKey : groupIdSet) { 588 int groupIndex = mGroupIdToGroupIndexMap.get(groupIdKey); 589 if (groupIndex > indexToRemove) { 590 mGroupIdToGroupIndexMap.put(groupIdKey, groupIndex - 1); 591 } 592 } 593 } 594 595 @Override selectTab(Tab tab)596 protected void selectTab(Tab tab) { 597 assert mAbsentSelectedTab == null; 598 int groupId = getRootId(tab); 599 if (mGroupIdToGroupMap.get(groupId) == null) { 600 mAbsentSelectedTab = tab; 601 } else { 602 mGroupIdToGroupMap.get(groupId).setLastShownTabId(tab.getId()); 603 mCurrentGroupIndex = mGroupIdToGroupIndexMap.get(groupId); 604 } 605 } 606 607 @Override reorder()608 protected void reorder() { 609 reorderGroup(TabGroup.INVALID_GROUP_ID); 610 611 TabModel tabModel = getTabModel(); 612 if (tabModel.index() == TabModel.INVALID_TAB_INDEX) { 613 mCurrentGroupIndex = TabModel.INVALID_TAB_INDEX; 614 } else { 615 selectTab(tabModel.getTabAt(tabModel.index())); 616 } 617 618 assert mGroupIdToGroupIndexMap.size() == mGroupIdToGroupMap.size(); 619 } 620 reorderGroup(int groupId)621 private void reorderGroup(int groupId) { 622 boolean reorderAllGroups = groupId == TabGroup.INVALID_GROUP_ID; 623 if (reorderAllGroups) { 624 mGroupIdToGroupIndexMap.clear(); 625 } 626 627 TabModel tabModel = getTabModel(); 628 for (int i = 0; i < tabModel.getCount(); i++) { 629 Tab tab = tabModel.getTabAt(i); 630 if (reorderAllGroups) { 631 groupId = getRootId(tab); 632 if (!mGroupIdToGroupIndexMap.containsKey(groupId)) { 633 mGroupIdToGroupIndexMap.put(groupId, mGroupIdToGroupIndexMap.size()); 634 } 635 } 636 mGroupIdToGroupMap.get(groupId).moveToEndInGroup(tab.getId()); 637 } 638 } 639 640 @Override resetFilterStateInternal()641 protected void resetFilterStateInternal() { 642 mGroupIdToGroupIndexMap.clear(); 643 mGroupIdToGroupMap.clear(); 644 mActualGroupCount = 0; 645 } 646 647 @Override removeTab(Tab tab)648 protected void removeTab(Tab tab) { 649 closeTab(tab); 650 } 651 652 @Override resetFilterState()653 protected void resetFilterState() { 654 mShouldRecordUma = false; 655 mIsResetting = true; 656 Map<Integer, Integer> groupIdToGroupLastShownTabId = new HashMap<>(); 657 for (int groupId : mGroupIdToGroupMap.keySet()) { 658 groupIdToGroupLastShownTabId.put( 659 groupId, mGroupIdToGroupMap.get(groupId).getLastShownTabId()); 660 } 661 662 super.resetFilterState(); 663 664 // Restore previous last shown tab ids after resetting filter state. 665 for (int groupId : mGroupIdToGroupMap.keySet()) { 666 // This happens when group with new groupId is formed after resetting filter state, i.e. 667 // when ungroup happens. Restoring last shown id of newly generated group is ignored. 668 if (!groupIdToGroupLastShownTabId.containsKey(groupId)) continue; 669 int lastShownId = groupIdToGroupLastShownTabId.get(groupId); 670 // This happens during continuous resetFilterState() calls caused by merging multiple 671 // tabs. Ignore the calls where the merge is not completed but the last shown tab has 672 // already been merged to new group. 673 if (!mGroupIdToGroupMap.get(groupId).contains(lastShownId)) continue; 674 mGroupIdToGroupMap.get(groupId).setLastShownTabId(lastShownId); 675 } 676 TabModel tabModel = getTabModel(); 677 if (tabModel.index() == TabModel.INVALID_TAB_INDEX) { 678 mCurrentGroupIndex = TabModel.INVALID_TAB_INDEX; 679 } else { 680 selectTab(tabModel.getTabAt(tabModel.index())); 681 } 682 mShouldRecordUma = true; 683 mIsResetting = false; 684 } 685 686 @Override shouldNotifyObserversOnSetIndex()687 protected boolean shouldNotifyObserversOnSetIndex() { 688 return mAbsentSelectedTab == null; 689 } 690 691 @Override didMoveTab(Tab tab, int newIndex, int curIndex)692 public void didMoveTab(Tab tab, int newIndex, int curIndex) { 693 // Ignore didMoveTab calls in tab restoring stage. 694 if (!isTabModelRestored()) return; 695 // Need to cache the flags before resetting the internal data map. 696 boolean isMergeTabToGroup = isMergeTabToGroup(tab); 697 boolean isMoveTabOutOfGroup = isMoveTabOutOfGroup(tab); 698 int groupIdBeforeMove = getGroupIdBeforeMove(tab, isMergeTabToGroup || isMoveTabOutOfGroup); 699 assert groupIdBeforeMove != TabGroup.INVALID_GROUP_ID; 700 TabGroup groupBeforeMove = mGroupIdToGroupMap.get(groupIdBeforeMove); 701 702 if (isMoveTabOutOfGroup) { 703 resetFilterState(); 704 705 int prevFilterIndex = mGroupIdToGroupIndexMap.get(groupIdBeforeMove); 706 for (Observer observer : mGroupFilterObserver) { 707 observer.didMoveTabOutOfGroup(tab, prevFilterIndex); 708 } 709 } else if (isMergeTabToGroup) { 710 resetFilterState(); 711 if (groupBeforeMove != null && groupBeforeMove.size() != 1) return; 712 713 TabGroup group = mGroupIdToGroupMap.get(getRootId(tab)); 714 for (Observer observer : mGroupFilterObserver) { 715 observer.didMergeTabToGroup(tab, group.getLastShownTabId()); 716 } 717 } else { 718 reorder(); 719 if (isMoveWithinGroup(tab, curIndex, newIndex)) { 720 for (Observer observer : mGroupFilterObserver) { 721 observer.didMoveWithinGroup(tab, curIndex, newIndex); 722 } 723 } else { 724 if (!hasFinishedMovingGroup(tab, newIndex)) return; 725 for (Observer observer : mGroupFilterObserver) { 726 observer.didMoveTabGroup(tab, curIndex, newIndex); 727 } 728 } 729 } 730 731 super.didMoveTab(tab, newIndex, curIndex); 732 } 733 setRootId(Tab tab, int id)734 private static void setRootId(Tab tab, int id) { 735 CriticalPersistedTabData.from(tab).setRootId(id); 736 } 737 getRootId(Tab tab)738 private static int getRootId(Tab tab) { 739 return CriticalPersistedTabData.from(tab).getRootId(); 740 } 741 isMoveTabOutOfGroup(Tab movedTab)742 private boolean isMoveTabOutOfGroup(Tab movedTab) { 743 return !mGroupIdToGroupMap.containsKey(getRootId(movedTab)); 744 } 745 isMergeTabToGroup(Tab tab)746 private boolean isMergeTabToGroup(Tab tab) { 747 if (!mGroupIdToGroupMap.containsKey(getRootId(tab))) return false; 748 TabGroup tabGroup = mGroupIdToGroupMap.get(getRootId(tab)); 749 return !tabGroup.contains(tab.getId()); 750 } 751 getGroupIdBeforeMove(Tab tabToMove, boolean isMoveToDifferentGroup)752 private int getGroupIdBeforeMove(Tab tabToMove, boolean isMoveToDifferentGroup) { 753 if (!isMoveToDifferentGroup) return getRootId(tabToMove); 754 755 Set<Integer> groupIdSet = mGroupIdToGroupMap.keySet(); 756 for (Integer groupIdKey : groupIdSet) { 757 if (mGroupIdToGroupMap.get(groupIdKey).contains(tabToMove.getId())) { 758 return groupIdKey; 759 } 760 } 761 762 return TabGroup.INVALID_GROUP_ID; 763 } 764 isMoveWithinGroup( Tab movedTab, int oldIndexInTabModel, int newIndexInTabModel)765 private boolean isMoveWithinGroup( 766 Tab movedTab, int oldIndexInTabModel, int newIndexInTabModel) { 767 int startIndex = Math.min(oldIndexInTabModel, newIndexInTabModel); 768 int endIndex = Math.max(oldIndexInTabModel, newIndexInTabModel); 769 for (int i = startIndex; i <= endIndex; i++) { 770 if (getRootId(getTabModel().getTabAt(i)) != getRootId(movedTab)) return false; 771 } 772 return true; 773 } 774 hasFinishedMovingGroup(Tab movedTab, int newIndexInTabModel)775 private boolean hasFinishedMovingGroup(Tab movedTab, int newIndexInTabModel) { 776 TabGroup tabGroup = mGroupIdToGroupMap.get(getRootId(movedTab)); 777 int offsetIndex = newIndexInTabModel - tabGroup.size() + 1; 778 if (offsetIndex < 0) return false; 779 780 for (int i = newIndexInTabModel; i >= offsetIndex; i--) { 781 if (getRootId(getTabModel().getTabAt(i)) != getRootId(movedTab)) return false; 782 } 783 return true; 784 } 785 786 // TabList implementation. 787 @Override isIncognito()788 public boolean isIncognito() { 789 return getTabModel().isIncognito(); 790 } 791 792 @Override index()793 public int index() { 794 return mCurrentGroupIndex; 795 } 796 797 @Override getCount()798 public int getCount() { 799 return mGroupIdToGroupMap.size(); 800 } 801 802 @Override getTabAt(int index)803 public Tab getTabAt(int index) { 804 if (index < 0 || index >= getCount()) return null; 805 int groupId = Tab.INVALID_TAB_ID; 806 Set<Integer> groupIdSet = mGroupIdToGroupIndexMap.keySet(); 807 for (Integer groupIdKey : groupIdSet) { 808 if (mGroupIdToGroupIndexMap.get(groupIdKey) == index) { 809 groupId = groupIdKey; 810 break; 811 } 812 } 813 if (groupId == Tab.INVALID_TAB_ID) return null; 814 815 return TabModelUtils.getTabById( 816 getTabModel(), mGroupIdToGroupMap.get(groupId).getLastShownTabId()); 817 } 818 819 @Override indexOf(Tab tab)820 public int indexOf(Tab tab) { 821 if (tab == null || tab.isIncognito() != isIncognito() 822 || getTabModel().indexOf(tab) == TabList.INVALID_TAB_INDEX) { 823 return TabList.INVALID_TAB_INDEX; 824 } 825 826 int groupId = getRootId(tab); 827 if (!mGroupIdToGroupIndexMap.containsKey(groupId)) return TabList.INVALID_TAB_INDEX; 828 return mGroupIdToGroupIndexMap.get(groupId); 829 } 830 831 @Override isClosurePending(int tabId)832 public boolean isClosurePending(int tabId) { 833 return getTabModel().isClosurePending(tabId); 834 } 835 836 @VisibleForTesting getGroupLastShownTabIdForTesting(int groupId)837 int getGroupLastShownTabIdForTesting(int groupId) { 838 return mGroupIdToGroupMap.get(groupId).getLastShownTabId(); 839 } 840 } 841