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