// Copyright 2020 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.base; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import org.chromium.build.BuildConfig; import java.util.ArrayList; import java.util.Collections; import java.util.Objects; import java.util.Set; import java.util.WeakHashMap; /** * UnownedUserDataKey is used in conjunction with a particular {@link UnownedUserData} as the key * for that when it is added to an {@link UnownedUserDataHost}. *

* This key is supposed to be private and not visible to other parts of the code base. Instead of * using the class as a key like in owned {@link org.chromium.base.UserData}, for {@link * UnownedUserData}, a particular object is used, ensuring that even if a class is visible outside * its own module, the instance of it as referenced from a {@link UnownedUserDataHost}, can not be * retrieved. *

* In practice, instances will typically be stored on this form: * *

{@code
 * public class Foo implements UnownedUserData {
 *     private static final UnownedUserDataKey KEY = new UnownedUserDataKey<>(Foo.class);
 *     ...
 * }
 * }
 * 
*

* This class and all its methods are final to ensure that no usage of the class leads to leaking * data about the object it is used as a key for. *

* It is OK to attach this key to as many different {@link UnownedUserDataHost} instances as * necessary, but doing so requires the client to invoke either {@link * #detachFromHost(UnownedUserDataHost)} or {@link #detachFromAllHosts(UnownedUserData)} during * cleanup. *

* Guarantees provided by this class together with {@link UnownedUserDataHost}: *

* * @param The Class this key is used for. * @see UnownedUserDataHost for more details on ownership and typical usage. * @see UnownedUserData for the marker interface used for this type of data. */ public final class UnownedUserDataKey { @NonNull private final Class mClazz; // A Set that uses WeakReference internally. private final Set mWeakHostAttachments = Collections.newSetFromMap(new WeakHashMap<>()); /** * Constructs a key to use for attaching to a particular {@link UnownedUserDataHost}. * * @param clazz The particular {@link UnownedUserData} class. */ public UnownedUserDataKey(@NonNull Class clazz) { mClazz = clazz; } @NonNull /* package */ final Class getValueClass() { return mClazz; } /** * Attaches the {@link UnownedUserData} object to the given {@link UnownedUserDataHost}, and * stores the host as a {@link WeakReference} to be able to detach from it later. * * @param host The host to attach the {@code object} to. * @param object The object to attach. */ public final void attachToHost(@NonNull UnownedUserDataHost host, @NonNull T object) { Objects.requireNonNull(object); // Setting a new value might lead to detachment of previously attached data, including // re-entry to this key, to happen before we update the {@link #mHostAttachments}. host.set(this, object); if (!isAttachedToHost(host)) { mWeakHostAttachments.add(host); } } /** * Attempts to retrieve the instance of the {@link UnownedUserData} from the given {@link * UnownedUserDataHost}. It will return {@code null} if the object is not attached to that * particular {@link UnownedUserDataHost} using this key, or the {@link UnownedUserData} has * been garbage collected. * * @param host The host to retrieve the {@link UnownedUserData} from. * @return The current {@link UnownedUserData} stored in the {@code host}, or {@code null}. */ @Nullable public final T retrieveDataFromHost(@NonNull UnownedUserDataHost host) { assertNoDestroyedAttachments(); for (UnownedUserDataHost attachedHost : mWeakHostAttachments) { if (host.equals(attachedHost)) { return host.get(this); } } return null; } /** * Detaches the key and object from the given host if it is attached with this key. It is OK to * call this for already detached objects. * * @param host The host to detach from. */ public final void detachFromHost(@NonNull UnownedUserDataHost host) { assertNoDestroyedAttachments(); for (UnownedUserDataHost attachedHost : new ArrayList<>(mWeakHostAttachments)) { if (host.equals(attachedHost)) { removeHostAttachment(attachedHost); } } } /** * Detaches the {@link UnownedUserData} from all hosts that it is currently attached to with * this key. It is OK to call this for already detached objects. * * @param object The object to detach from all hosts. */ public final void detachFromAllHosts(@NonNull T object) { assertNoDestroyedAttachments(); for (UnownedUserDataHost attachedHost : new ArrayList<>(mWeakHostAttachments)) { if (object.equals(attachedHost.get(this))) { removeHostAttachment(attachedHost); } } } /** * Checks if the {@link UnownedUserData} is currently attached to the given host with this key. * * @param host The host to check if the {@link UnownedUserData} is attached to. * @return true if currently attached, false otherwise. */ public final boolean isAttachedToHost(@NonNull UnownedUserDataHost host) { T t = retrieveDataFromHost(host); return t != null; } /** * @return Whether the {@link UnownedUserData} is currently attached to any hosts with this key. */ public final boolean isAttachedToAnyHost(@NonNull T object) { return getHostAttachmentCount(object) > 0; } @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) /* package */ int getHostAttachmentCount(@NonNull T object) { assertNoDestroyedAttachments(); int ret = 0; for (UnownedUserDataHost attachedHost : mWeakHostAttachments) { if (object.equals(attachedHost.get(this))) { ret++; } } return ret; } private void removeHostAttachment(UnownedUserDataHost host) { host.remove(this); mWeakHostAttachments.remove(host); } private void assertNoDestroyedAttachments() { if (BuildConfig.ENABLE_ASSERTS) { for (UnownedUserDataHost attachedHost : mWeakHostAttachments) { if (attachedHost.isDestroyed()) { assert false : "Host should have been removed already."; throw new IllegalStateException(); } } } } }