1 // Copyright 2020 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.tab.state; 6 7 import android.graphics.Color; 8 import android.text.TextUtils; 9 10 import androidx.annotation.Nullable; 11 import androidx.annotation.VisibleForTesting; 12 13 import com.google.protobuf.ByteString; 14 import com.google.protobuf.InvalidProtocolBufferException; 15 16 import org.chromium.base.Callback; 17 import org.chromium.base.Log; 18 import org.chromium.base.ObserverList; 19 import org.chromium.base.TraceEvent; 20 import org.chromium.chrome.browser.tab.Tab; 21 import org.chromium.chrome.browser.tab.TabLaunchType; 22 import org.chromium.chrome.browser.tab.WebContentsState; 23 import org.chromium.chrome.browser.tab.WebContentsStateBridge; 24 import org.chromium.chrome.browser.tab.proto.CriticalPersistedTabData.CriticalPersistedTabDataProto; 25 import org.chromium.components.embedder_support.util.UrlConstants; 26 import org.chromium.components.embedder_support.util.UrlUtilities; 27 import org.chromium.content_public.browser.LoadUrlParams; 28 import org.chromium.content_public.common.Referrer; 29 import org.chromium.url.GURL; 30 31 import java.nio.ByteBuffer; 32 import java.util.Locale; 33 34 /** 35 * Data which is core to the app and must be retrieved as quickly as possible on startup. 36 */ 37 public class CriticalPersistedTabData extends PersistedTabData { 38 private static final String TAG = "CriticalPTD"; 39 private static final Class<CriticalPersistedTabData> USER_DATA_KEY = 40 CriticalPersistedTabData.class; 41 42 private static final int UNSPECIFIED_THEME_COLOR = Color.TRANSPARENT; 43 public static final long INVALID_TIMESTAMP = -1; 44 45 /** 46 * Title of the ContentViews webpage. 47 */ 48 private String mTitle; 49 50 /** 51 * URL of the page currently loading. Used as a fall-back in case tab restore fails. 52 */ 53 private GURL mUrl; 54 private int mParentId; 55 private int mRootId; 56 private long mTimestampMillis; 57 /** 58 * Navigation state of the WebContents as returned by nativeGetContentsStateAsByteBuffer(), 59 * stored to be inflated on demand using unfreezeContents(). If this is not null, there is no 60 * WebContents around. Upon tab switch WebContents will be unfrozen and the variable will be set 61 * to null. 62 */ 63 private WebContentsState mWebContentsState; 64 private int mContentStateVersion; 65 private String mOpenerAppId; 66 private int mThemeColor; 67 /** 68 * Saves how this tab was initially launched so that we can record metrics on how the 69 * tab was created. This is different than {@link Tab#getLaunchType()}, since {@link 70 * Tab#getLaunchType()} will be overridden to "FROM_RESTORE" during tab restoration. 71 */ 72 private @Nullable @TabLaunchType Integer mTabLaunchTypeAtCreation; 73 74 private ObserverList<CriticalPersistedTabDataObserver> mObservers = 75 new ObserverList<CriticalPersistedTabDataObserver>(); 76 private boolean mShouldSaveForTesting; 77 78 @VisibleForTesting CriticalPersistedTabData(Tab tab)79 protected CriticalPersistedTabData(Tab tab) { 80 super(tab, 81 PersistedTabDataConfiguration.get(CriticalPersistedTabData.class, tab.isIncognito()) 82 .getStorage(), 83 PersistedTabDataConfiguration.get(CriticalPersistedTabData.class, tab.isIncognito()) 84 .getId()); 85 } 86 87 /** 88 * @param tab {@link Tab} {@link CriticalPersistedTabData} is being stored for 89 * @param parentId parent identiifer for the {@link Tab} 90 * @param rootId root identifier for the {@link Tab} 91 * @param timestampMillis creation timestamp for the {@link Tab} 92 * @param contentStateBytes content state bytes for the {@link Tab} 93 * @param contentStateVersion content state version for the {@link Tab} 94 * @param openerAppId identifier for app opener 95 * @param themeColor theme color 96 * @param launchTypeAtCreation launch type at creation 97 * @param persistedTabDataStorage storage for {@link PersistedTabData} 98 * @param persistedTabDataId identifier for {@link PersistedTabData} in storage 99 */ 100 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) CriticalPersistedTabData(Tab tab, String url, String title, int parentId, int rootId, long timestampMillis, WebContentsState webContentsState, int contentStateVersion, String openerAppId, int themeColor, @Nullable @TabLaunchType Integer launchTypeAtCreation)101 CriticalPersistedTabData(Tab tab, String url, String title, int parentId, int rootId, 102 long timestampMillis, WebContentsState webContentsState, int contentStateVersion, 103 String openerAppId, int themeColor, 104 @Nullable @TabLaunchType Integer launchTypeAtCreation) { 105 this(tab); 106 mUrl = url == null || url.isEmpty() ? GURL.emptyGURL() : new GURL(url); 107 mTitle = title; 108 mParentId = parentId; 109 mRootId = rootId; 110 mTimestampMillis = timestampMillis; 111 mWebContentsState = webContentsState; 112 mContentStateVersion = contentStateVersion; 113 mOpenerAppId = openerAppId; 114 mThemeColor = themeColor; 115 mTabLaunchTypeAtCreation = launchTypeAtCreation; 116 } 117 118 /** 119 * @param tab {@link Tab} {@link CriticalPersistedTabData} is being stored for 120 * @param data serialized {@link CriticalPersistedTabData} 121 * @param storage {@link PersistedTabDataStorage} for {@link PersistedTabData} 122 * @param persistedTabDataId unique identifier for {@link PersistedTabData} in 123 * storage 124 * This constructor is public because that is needed for the reflection 125 * used in PersistedTabData.java 126 */ 127 @VisibleForTesting CriticalPersistedTabData( Tab tab, byte[] data, PersistedTabDataStorage storage, String persistedTabDataId)128 protected CriticalPersistedTabData( 129 Tab tab, byte[] data, PersistedTabDataStorage storage, String persistedTabDataId) { 130 super(tab, storage, persistedTabDataId); 131 deserializeAndLog(data); 132 } 133 134 /** 135 * TODO(crbug.com/1096142) asynchronous from can be removed 136 * Acquire {@link CriticalPersistedTabData} from storage 137 * @param tab {@link Tab} {@link CriticalPersistedTabData} is being stored for. 138 * At a minimum this needs to be a frozen {@link Tab} with an identifier 139 * and isIncognito values set. 140 * @param callback callback to pass {@link PersistedTabData} back in 141 */ from(Tab tab, Callback<CriticalPersistedTabData> callback)142 public static void from(Tab tab, Callback<CriticalPersistedTabData> callback) { 143 PersistedTabData.from(tab, 144 (data, storage, id) 145 -> { return new CriticalPersistedTabData(tab, data, storage, id); }, 146 (supplierCallback) 147 -> supplierCallback.onResult( 148 tab.isInitialized() ? CriticalPersistedTabData.build(tab) : null), 149 CriticalPersistedTabData.class, callback); 150 } 151 152 /** 153 * Acquire {@link CriticalPersistedTabData} from a {@link Tab} or create if it doesn't exist 154 * @param corresponding {@link Tab} for which {@link CriticalPersistedTabData} is sought 155 * @return acquired or created {@link CriticalPersistedTabData} 156 */ from(Tab tab)157 public static CriticalPersistedTabData from(Tab tab) { 158 return PersistedTabData.from( 159 tab, CriticalPersistedTabData.class, () -> { return build(tab); }); 160 } 161 162 /** 163 * Synchronously restore serialized {@link CriticalPersistedTabData} 164 * @param tabId identifier for the {@link Tab} 165 * @param isIncognito true if the {@link Tab} is incognito 166 * @return serialized {@link CriticalPersistedTabData} 167 * TODO(crbug.com/1119452) rethink CriticalPersistedTabData contract 168 */ restore(int tabId, boolean isIncognito)169 public static byte[] restore(int tabId, boolean isIncognito) { 170 PersistedTabDataConfiguration config = 171 PersistedTabDataConfiguration.get(CriticalPersistedTabData.class, isIncognito); 172 return config.getStorage().restore(tabId, config.getId()); 173 } 174 175 /** 176 * Asynchronously restore serialized {@link CriticalPersistedTabData} 177 * @param tabId identifier for the {@link Tab} 178 * @param isIncognito true if the {@link Tab} is incognito 179 * @param callback the serialized {@link CriticalPersistedTabData} is passed back in 180 */ restore(int tabId, boolean isIncognito, Callback<byte[]> callback)181 public static void restore(int tabId, boolean isIncognito, Callback<byte[]> callback) { 182 PersistedTabDataConfiguration config = 183 PersistedTabDataConfiguration.get(CriticalPersistedTabData.class, isIncognito); 184 config.getStorage().restore(tabId, config.getId(), callback); 185 } 186 187 /** 188 * @param tab {@link Tab} associated with the {@link CriticalPersistedTabData} 189 * @param serialized {@link CriticalPersistedTabData} in serialized form 190 * @param isCriticalPersistedTabDataEnabled true if CriticalPersistedData is enabled 191 * as the storage/retrieval method 192 */ build(Tab tab, byte[] serialized, boolean isStorageRetrievalEnabled)193 public static void build(Tab tab, byte[] serialized, boolean isStorageRetrievalEnabled) { 194 CriticalPersistedTabData res = PersistedTabData.build(tab, (data, storage, id) -> { 195 return new CriticalPersistedTabData(tab, data, storage, id); 196 }, serialized, CriticalPersistedTabData.class); 197 } 198 199 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) build(Tab tab)200 public static CriticalPersistedTabData build(Tab tab) { 201 // CriticalPersistedTabData is initialized with default values 202 CriticalPersistedTabData criticalPersistedTabData = 203 new CriticalPersistedTabData(tab, "", "", Tab.INVALID_TAB_ID, tab.getId(), 204 INVALID_TIMESTAMP, null, -1, "", UNSPECIFIED_THEME_COLOR, null); 205 criticalPersistedTabData.save(); 206 return criticalPersistedTabData; 207 } 208 209 @Override deserialize(@ullable byte[] bytes)210 boolean deserialize(@Nullable byte[] bytes) { 211 try (TraceEvent e = TraceEvent.scoped("CriticalPersistedTabData.Deserialize")) { 212 CriticalPersistedTabDataProto criticalPersistedTabDataProto = 213 CriticalPersistedTabDataProto.parseFrom(bytes); 214 mParentId = criticalPersistedTabDataProto.getParentId(); 215 mRootId = criticalPersistedTabDataProto.getRootId(); 216 mTimestampMillis = criticalPersistedTabDataProto.getTimestampMillis(); 217 byte[] webContentsStateBytes = 218 criticalPersistedTabDataProto.getWebContentsStateBytes().toByteArray(); 219 mWebContentsState = 220 new WebContentsState(ByteBuffer.allocateDirect(webContentsStateBytes.length)); 221 mWebContentsState.buffer().put(webContentsStateBytes); 222 mWebContentsState.setVersion(WebContentsState.CONTENTS_STATE_CURRENT_VERSION); 223 mUrl = mWebContentsState.getVirtualUrlFromState() == null 224 ? GURL.emptyGURL() 225 : new GURL(mWebContentsState.getVirtualUrlFromState()); 226 mTitle = mWebContentsState.getDisplayTitleFromState(); 227 mContentStateVersion = criticalPersistedTabDataProto.getContentStateVersion(); 228 mOpenerAppId = TextUtils.isEmpty(criticalPersistedTabDataProto.getOpenerAppId()) 229 ? null 230 : criticalPersistedTabDataProto.getOpenerAppId(); 231 mThemeColor = criticalPersistedTabDataProto.getThemeColor(); 232 mTabLaunchTypeAtCreation = 233 getLaunchType(criticalPersistedTabDataProto.getLaunchTypeAtCreation()); 234 return true; 235 } catch (InvalidProtocolBufferException e) { 236 Log.e(TAG, 237 String.format(Locale.ENGLISH, 238 "There was a problem deserializing Tab %d. Details: %s", mTab.getId(), 239 e.getMessage())); 240 } 241 return false; 242 } 243 244 @Override getUmaTag()245 public String getUmaTag() { 246 return "Critical"; 247 } 248 getLaunchType( CriticalPersistedTabDataProto.LaunchTypeAtCreation protoLaunchType)249 private static @Nullable @TabLaunchType Integer getLaunchType( 250 CriticalPersistedTabDataProto.LaunchTypeAtCreation protoLaunchType) { 251 switch (protoLaunchType) { 252 case FROM_LINK: 253 return TabLaunchType.FROM_LINK; 254 case FROM_EXTERNAL_APP: 255 return TabLaunchType.FROM_EXTERNAL_APP; 256 case FROM_CHROME_UI: 257 return TabLaunchType.FROM_CHROME_UI; 258 case FROM_RESTORE: 259 return TabLaunchType.FROM_RESTORE; 260 case FROM_LONGPRESS_FOREGROUND: 261 return TabLaunchType.FROM_LONGPRESS_FOREGROUND; 262 case FROM_LONGPRESS_BACKGROUND: 263 return TabLaunchType.FROM_LONGPRESS_BACKGROUND; 264 case FROM_REPARENTING: 265 return TabLaunchType.FROM_REPARENTING; 266 case FROM_LAUNCHER_SHORTCUT: 267 return TabLaunchType.FROM_LAUNCHER_SHORTCUT; 268 case FROM_SPECULATIVE_BACKGROUND_CREATION: 269 return TabLaunchType.FROM_SPECULATIVE_BACKGROUND_CREATION; 270 case FROM_BROWSER_ACTIONS: 271 return TabLaunchType.FROM_BROWSER_ACTIONS; 272 case FROM_LAUNCH_NEW_INCOGNITO_TAB: 273 return TabLaunchType.FROM_LAUNCH_NEW_INCOGNITO_TAB; 274 case FROM_STARTUP: 275 return TabLaunchType.FROM_STARTUP; 276 case FROM_START_SURFACE: 277 return TabLaunchType.FROM_START_SURFACE; 278 case SIZE: 279 return TabLaunchType.SIZE; 280 default: 281 assert false : "Unexpected deserialization of LaunchAtCreationType: " 282 + protoLaunchType; 283 // shouldn't happen 284 return null; 285 } 286 } 287 getLaunchType( @ullable @abLaunchType Integer protoLaunchType)288 private static CriticalPersistedTabDataProto.LaunchTypeAtCreation getLaunchType( 289 @Nullable @TabLaunchType Integer protoLaunchType) { 290 if (protoLaunchType == null) { 291 return CriticalPersistedTabDataProto.LaunchTypeAtCreation.UNKNOWN; 292 } 293 switch (protoLaunchType) { 294 case TabLaunchType.FROM_LINK: 295 return CriticalPersistedTabDataProto.LaunchTypeAtCreation.FROM_LINK; 296 case TabLaunchType.FROM_EXTERNAL_APP: 297 return CriticalPersistedTabDataProto.LaunchTypeAtCreation.FROM_EXTERNAL_APP; 298 case TabLaunchType.FROM_CHROME_UI: 299 return CriticalPersistedTabDataProto.LaunchTypeAtCreation.FROM_CHROME_UI; 300 case TabLaunchType.FROM_RESTORE: 301 return CriticalPersistedTabDataProto.LaunchTypeAtCreation.FROM_RESTORE; 302 case TabLaunchType.FROM_LONGPRESS_FOREGROUND: 303 return CriticalPersistedTabDataProto.LaunchTypeAtCreation.FROM_LONGPRESS_FOREGROUND; 304 case TabLaunchType.FROM_LONGPRESS_BACKGROUND: 305 return CriticalPersistedTabDataProto.LaunchTypeAtCreation.FROM_LONGPRESS_BACKGROUND; 306 case TabLaunchType.FROM_REPARENTING: 307 return CriticalPersistedTabDataProto.LaunchTypeAtCreation.FROM_REPARENTING; 308 case TabLaunchType.FROM_LAUNCHER_SHORTCUT: 309 return CriticalPersistedTabDataProto.LaunchTypeAtCreation.FROM_LAUNCHER_SHORTCUT; 310 case TabLaunchType.FROM_SPECULATIVE_BACKGROUND_CREATION: 311 return CriticalPersistedTabDataProto.LaunchTypeAtCreation 312 .FROM_SPECULATIVE_BACKGROUND_CREATION; 313 case TabLaunchType.FROM_BROWSER_ACTIONS: 314 return CriticalPersistedTabDataProto.LaunchTypeAtCreation.FROM_BROWSER_ACTIONS; 315 case TabLaunchType.FROM_LAUNCH_NEW_INCOGNITO_TAB: 316 return CriticalPersistedTabDataProto.LaunchTypeAtCreation 317 .FROM_LAUNCH_NEW_INCOGNITO_TAB; 318 case TabLaunchType.FROM_STARTUP: 319 return CriticalPersistedTabDataProto.LaunchTypeAtCreation.FROM_STARTUP; 320 case TabLaunchType.FROM_START_SURFACE: 321 return CriticalPersistedTabDataProto.LaunchTypeAtCreation.FROM_START_SURFACE; 322 case TabLaunchType.SIZE: 323 return CriticalPersistedTabDataProto.LaunchTypeAtCreation.SIZE; 324 default: 325 assert false : "Unexpected serialization of LaunchAtCreationType: " 326 + protoLaunchType; 327 // shouldn't happen 328 return CriticalPersistedTabDataProto.LaunchTypeAtCreation.UNKNOWN; 329 } 330 } 331 getWebContentsStateFromTab(Tab tab)332 private static WebContentsState getWebContentsStateFromTab(Tab tab) { 333 // Native call returns null when buffer allocation needed to serialize the state failed. 334 ByteBuffer buffer = getWebContentsStateAsByteBuffer(tab); 335 if (buffer == null) return null; 336 337 WebContentsState state = new WebContentsState(buffer); 338 state.setVersion(WebContentsState.CONTENTS_STATE_CURRENT_VERSION); 339 return state; 340 } 341 342 /** Returns an ByteBuffer representing the state of the Tab's WebContents. */ getWebContentsStateAsByteBuffer(Tab tab)343 private static ByteBuffer getWebContentsStateAsByteBuffer(Tab tab) { 344 LoadUrlParams pendingLoadParams = tab.getPendingLoadParams(); 345 if (pendingLoadParams == null) { 346 return WebContentsStateBridge.getContentsStateAsByteBuffer(tab.getWebContents()); 347 } else { 348 Referrer referrer = pendingLoadParams.getReferrer(); 349 return WebContentsStateBridge.createSingleNavigationStateAsByteBuffer( 350 pendingLoadParams.getUrl(), referrer != null ? referrer.getUrl() : null, 351 // Policy will be ignored for null referrer url, 0 is just a placeholder. 352 referrer != null ? referrer.getPolicy() : 0, 353 pendingLoadParams.getInitiatorOrigin(), tab.isIncognito()); 354 } 355 } 356 357 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) 358 @Override serialize()359 public byte[] serialize() { 360 try (TraceEvent e = TraceEvent.scoped("CriticalPersistedTabData.Serialize")) { 361 WebContentsState webContentsState = mWebContentsState; 362 if (webContentsState == null) { 363 webContentsState = getWebContentsStateFromTab(mTab); 364 } 365 return CriticalPersistedTabDataProto.newBuilder() 366 .setParentId(mParentId) 367 .setRootId(mRootId) 368 .setTimestampMillis(mTimestampMillis) 369 .setWebContentsStateBytes(webContentsState == null 370 ? ByteString.EMPTY 371 : ByteString.copyFrom( 372 getContentStateByteArray(webContentsState.buffer()))) 373 .setContentStateVersion(mContentStateVersion) 374 .setOpenerAppId(mOpenerAppId == null ? "" : mOpenerAppId) 375 .setThemeColor(mThemeColor) 376 .setLaunchTypeAtCreation(getLaunchType(mTabLaunchTypeAtCreation)) 377 .build() 378 .toByteArray(); 379 } 380 } 381 getContentStateByteArray(ByteBuffer buffer)382 protected static byte[] getContentStateByteArray(ByteBuffer buffer) { 383 byte[] contentsStateBytes = new byte[buffer.limit()]; 384 buffer.rewind(); 385 buffer.get(contentsStateBytes); 386 return contentsStateBytes; 387 } 388 389 @Override save()390 public void save() { 391 if (shouldSave()) { 392 super.save(); 393 } 394 } 395 396 /** 397 * Encapsulates use cases where saving is disabled - as taken from TabPersistentStore.java to 398 * ensure feature parity. 399 * @return 400 */ 401 @VisibleForTesting shouldSave()402 protected boolean shouldSave() { 403 if (mShouldSaveForTesting) { 404 return true; 405 } 406 if (getUrl() == null || TextUtils.isEmpty(getUrl().getSpec())) { 407 return false; 408 } 409 if (UrlUtilities.isNTPUrl(getUrl().getSpec()) && !mTab.canGoBack() 410 && !mTab.canGoForward()) { 411 return false; 412 } 413 if (isTabUrlContentScheme(getUrl().getSpec())) { 414 return false; 415 } 416 return true; 417 } 418 isTabUrlContentScheme(String url)419 private boolean isTabUrlContentScheme(String url) { 420 return url != null && url.startsWith(UrlConstants.CONTENT_SCHEME); 421 } 422 423 @Override destroy()424 public void destroy() {} 425 426 /** 427 * @return title of the {@link Tab} 428 */ getTitle()429 public String getTitle() { 430 return mTitle; 431 } 432 433 /** 434 * @param title of the {@link Tab} to set 435 */ setTitle(String title)436 public void setTitle(String title) { 437 mTitle = title; 438 save(); 439 } 440 441 /** 442 * @return {@link GURL} for the {@link Tab} 443 */ getUrl()444 public GURL getUrl() { 445 return mUrl; 446 } 447 448 /** 449 * Set {@link GURL} for the {@link Tab} 450 * @param url to set 451 */ setUrl(GURL url)452 public void setUrl(GURL url) { 453 mUrl = url; 454 save(); 455 } 456 457 /** 458 * @return identifier for the {@link Tab} 459 */ getTabId()460 public int getTabId() { 461 return mTab.getId(); 462 } 463 464 /** 465 * @return root identifier for the {@link Tab} 466 */ getRootId()467 public int getRootId() { 468 return mRootId; 469 } 470 471 /** 472 * Set root id 473 */ setRootId(int rootId)474 public void setRootId(int rootId) { 475 if (mRootId == rootId) return; 476 // TODO(crbug.com/1059640) add in setters for all mutable fields 477 mRootId = rootId; 478 for (CriticalPersistedTabDataObserver observer : mObservers) { 479 observer.onRootIdChanged(mTab, rootId); 480 } 481 mTab.setIsTabStateDirty(true); 482 save(); 483 } 484 485 /** 486 * @return parent identifier for the {@link Tab} 487 */ getParentId()488 public int getParentId() { 489 return mParentId; 490 } 491 492 /** 493 * Set parent identifier for the {@link Tab} 494 */ setParentId(int parentId)495 public void setParentId(int parentId) { 496 mParentId = parentId; 497 save(); 498 } 499 500 /** 501 * @return timestamp in milliseconds for the {@link Tab} 502 */ getTimestampMillis()503 public long getTimestampMillis() { 504 return mTimestampMillis; 505 } 506 507 /** 508 * set the timetsamp 509 * @param timestamp the timestamp 510 */ setTimestampMillis(long timestamp)511 public void setTimestampMillis(long timestamp) { 512 mTimestampMillis = timestamp; 513 save(); 514 } 515 516 /** 517 * @return content state bytes for the {@link Tab} 518 */ getWebContentsState()519 public WebContentsState getWebContentsState() { 520 return mWebContentsState; 521 } 522 setWebContentsState(WebContentsState webContentsState)523 public void setWebContentsState(WebContentsState webContentsState) { 524 mWebContentsState = webContentsState; 525 save(); 526 } 527 528 /** 529 * @return content state version for the {@link Tab} 530 */ getContentStateVersion()531 public int getContentStateVersion() { 532 return mContentStateVersion; 533 } 534 535 /** 536 * @return opener app id 537 */ getOpenerAppId()538 public String getOpenerAppId() { 539 return mOpenerAppId; 540 } 541 542 /** 543 * @return theme color 544 */ getThemeColor()545 public int getThemeColor() { 546 return mThemeColor; 547 } 548 549 /** 550 * @return launch type at creation 551 */ getTabLaunchTypeAtCreation()552 public @Nullable @TabLaunchType Integer getTabLaunchTypeAtCreation() { 553 return mTabLaunchTypeAtCreation; 554 } 555 setLaunchTypeAtCreation(@ullable @abLaunchType Integer launchTypeAtCreation)556 public void setLaunchTypeAtCreation(@Nullable @TabLaunchType Integer launchTypeAtCreation) { 557 mTabLaunchTypeAtCreation = launchTypeAtCreation; 558 save(); 559 } 560 561 /** 562 * Add a {@link CriticalPersistedTabDataObserver} 563 * @param criticalPersistedTabDataObserver the observer 564 */ addObserver(CriticalPersistedTabDataObserver criticalPersistedTabDataObserver)565 public void addObserver(CriticalPersistedTabDataObserver criticalPersistedTabDataObserver) { 566 mObservers.addObserver(criticalPersistedTabDataObserver); 567 } 568 569 /** 570 * Remove a {@link CriticalPersistedTabDataObserver} 571 * @param criticalPersistedTabDataObserver the observer 572 */ removeObserver(CriticalPersistedTabDataObserver criticalPersistedTabDataObserver)573 public void removeObserver(CriticalPersistedTabDataObserver criticalPersistedTabDataObserver) { 574 mObservers.removeObserver(criticalPersistedTabDataObserver); 575 } 576 577 @VisibleForTesting setShouldSaveForTesting(boolean shouldSaveForTesting)578 protected void setShouldSaveForTesting(boolean shouldSaveForTesting) { 579 mShouldSaveForTesting = shouldSaveForTesting; 580 } 581 } 582