// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.tab; import android.content.Context; import android.content.SharedPreferences; import org.chromium.base.ContextUtils; import org.chromium.base.VisibleForTesting; import org.chromium.chrome.browser.tabmodel.TabModel; import java.util.concurrent.atomic.AtomicInteger; /** * Maintains a monotonically increasing ID that is used for uniquely identifying {@link Tab}s. This * class is responsible for ensuring that Tabs created in the same process, across every * {@link TabModel}, are allocated a unique ID. Note that only the browser process should be * generating Tab IDs to prevent collisions. * * Calling {@link TabIdManager#incrementIdCounterTo(int)} will ensure new {@link Tab}s get IDs * greater than or equal to the parameter passed to that method. This should be used when doing * things like loading persisted {@link Tab}s from disk on process start to ensure all new * {@link Tab}s don't have id collision. * * TODO(dfalcantara): Tab ID generation prior to M45 is haphazard and dependent on which Activity is * started first. Unify the ways the maximum Tab ID is set (crbug.com/502384). */ public class TabIdManager { @VisibleForTesting public static final String PREF_NEXT_ID = "org.chromium.chrome.browser.tab.TabIdManager.NEXT_ID"; private static final Object INSTANCE_LOCK = new Object(); private static TabIdManager sInstance; private final Context mContext; private final AtomicInteger mIdCounter = new AtomicInteger(); private SharedPreferences mPreferences; /** Returns the Singleton instance of the TabIdManager. */ public static TabIdManager getInstance() { return getInstance(ContextUtils.getApplicationContext()); } /** Returns the Singleton instance of the TabIdManager. */ @VisibleForTesting static TabIdManager getInstance(Context context) { synchronized (INSTANCE_LOCK) { if (sInstance == null) sInstance = new TabIdManager(context); } return sInstance; } /** * Validates {@code id} and increments the internal counter to make sure future ids don't * collide. * @param id The current id. May be {@link #INVALID_TAB_ID}. * @return A new id if {@code id} was {@link #INVALID_TAB_ID}, or {@code id}. */ public final int generateValidId(int id) { if (id == Tab.INVALID_TAB_ID) id = mIdCounter.getAndIncrement(); incrementIdCounterTo(id + 1); return id; } /** * Ensures the counter is at least as high as the specified value. The counter should always * point to an unused ID (which will be handed out next time a request comes in). Exposed so * that anything externally loading tabs and ids can set enforce new tabs start at the correct * id. * TODO(dfalcantara): Reduce the visibility of this method once all TabModels are united in how * the IDs are assigned (crbug.com/502384). * @param id The minimum id we should hand out to the next new tab. */ public final void incrementIdCounterTo(int id) { int diff = id - mIdCounter.get(); if (diff < 0) return; // It's possible idCounter has been incremented between the get and the add but that's OK -- // in the worst case mIdCounter will just be overly incremented. mIdCounter.addAndGet(diff); mPreferences.edit().putInt(PREF_NEXT_ID, mIdCounter.get()).apply(); } private TabIdManager(Context context) { mContext = context; // Read the shared preference. This has to be done on the critical path to ensure that the // myriad Activities that serve as entries into Chrome are all synchronized on the correct // maximum Tab ID. mPreferences = ContextUtils.getAppSharedPreferences(); mIdCounter.set(mPreferences.getInt(PREF_NEXT_ID, 0)); } }