198 lines
7.5 KiB
Java
198 lines
7.5 KiB
Java
// 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}.
|
|
* <p>
|
|
* 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.
|
|
* <p>
|
|
* In practice, instances will typically be stored on this form:
|
|
*
|
|
* <pre>{@code
|
|
* public class Foo implements UnownedUserData {
|
|
* private static final UnownedUserDataKey<Foo> KEY = new UnownedUserDataKey<>(Foo.class);
|
|
* ...
|
|
* }
|
|
* }
|
|
* </pre>
|
|
* <p>
|
|
* 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.
|
|
* <p>
|
|
* 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.
|
|
* <p>
|
|
* Guarantees provided by this class together with {@link UnownedUserDataHost}:
|
|
* <ul>
|
|
* <li> One key can be used for multiple {@link UnownedUserData}s.
|
|
* <li> One key can be attached to multiple {@link UnownedUserDataHost}s.
|
|
* <li> One key can be attached to a particular {@link UnownedUserDataHost} only once. This ensures
|
|
* a pair of {@link UnownedUserDataHost} and UnownedUserDataKey can only refer to a single
|
|
* UnownedUserData.
|
|
* <li> When a {@link UnownedUserData} is detached from a particular host, it is informed of this,
|
|
* except if it has been garbage collected.
|
|
* <li> When an {@link UnownedUserData} object is replaced with a different {@link UnownedUserData}
|
|
* using the same UnownedUserDataKey, the former is detached.
|
|
* </ul>
|
|
*
|
|
* @param <T> 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<T extends UnownedUserData> {
|
|
@NonNull private final Class<T> mClazz;
|
|
// A Set that uses WeakReference<UnownedUserDataHost> internally.
|
|
private final Set<UnownedUserDataHost> 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<T> clazz) {
|
|
mClazz = clazz;
|
|
}
|
|
|
|
@NonNull
|
|
/* package */ final Class<T> 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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|