1 // Copyright 2020 The Chromium Authors 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.base; 6 7 import androidx.annotation.NonNull; 8 import androidx.annotation.Nullable; 9 import androidx.annotation.VisibleForTesting; 10 11 import org.chromium.build.BuildConfig; 12 13 import java.util.ArrayList; 14 import java.util.Collections; 15 import java.util.Objects; 16 import java.util.Set; 17 import java.util.WeakHashMap; 18 19 /** 20 * UnownedUserDataKey is used in conjunction with a particular {@link UnownedUserData} as the key 21 * for that when it is added to an {@link UnownedUserDataHost}. 22 * <p> 23 * This key is supposed to be private and not visible to other parts of the code base. Instead of 24 * using the class as a key like in owned {@link org.chromium.base.UserData}, for {@link 25 * UnownedUserData}, a particular object is used, ensuring that even if a class is visible outside 26 * its own module, the instance of it as referenced from a {@link UnownedUserDataHost}, can not be 27 * retrieved. 28 * <p> 29 * In practice, instances will typically be stored on this form: 30 * 31 * <pre>{@code 32 * public class Foo implements UnownedUserData { 33 * private static final UnownedUserDataKey<Foo> KEY = new UnownedUserDataKey<>(Foo.class); 34 * ... 35 * } 36 * } 37 * </pre> 38 * <p> 39 * This class and all its methods are final to ensure that no usage of the class leads to leaking 40 * data about the object it is used as a key for. 41 * <p> 42 * It is OK to attach this key to as many different {@link UnownedUserDataHost} instances as 43 * necessary, but doing so requires the client to invoke either {@link 44 * #detachFromHost(UnownedUserDataHost)} or {@link #detachFromAllHosts(UnownedUserData)} during 45 * cleanup. 46 * <p> 47 * Guarantees provided by this class together with {@link UnownedUserDataHost}: 48 * <ul> 49 * <li> One key can be used for multiple {@link UnownedUserData}s. 50 * <li> One key can be attached to multiple {@link UnownedUserDataHost}s. 51 * <li> One key can be attached to a particular {@link UnownedUserDataHost} only once. This ensures 52 * a pair of {@link UnownedUserDataHost} and UnownedUserDataKey can only refer to a single 53 * UnownedUserData. 54 * <li> When a {@link UnownedUserData} is detached from a particular host, it is informed of this, 55 * except if it has been garbage collected. 56 * <li> When an {@link UnownedUserData} object is replaced with a different {@link UnownedUserData} 57 * using the same UnownedUserDataKey, the former is detached. 58 * </ul> 59 * 60 * @param <T> The Class this key is used for. 61 * @see UnownedUserDataHost for more details on ownership and typical usage. 62 * @see UnownedUserData for the marker interface used for this type of data. 63 */ 64 public final class UnownedUserDataKey<T extends UnownedUserData> { 65 @NonNull private final Class<T> mClazz; 66 // A Set that uses WeakReference<UnownedUserDataHost> internally. 67 private final Set<UnownedUserDataHost> mWeakHostAttachments = 68 Collections.newSetFromMap(new WeakHashMap<>()); 69 70 /** 71 * Constructs a key to use for attaching to a particular {@link UnownedUserDataHost}. 72 * 73 * @param clazz The particular {@link UnownedUserData} class. 74 */ UnownedUserDataKey(@onNull Class<T> clazz)75 public UnownedUserDataKey(@NonNull Class<T> clazz) { 76 mClazz = clazz; 77 } 78 79 @NonNull getValueClass()80 /* package */ final Class<T> getValueClass() { 81 return mClazz; 82 } 83 84 /** 85 * Attaches the {@link UnownedUserData} object to the given {@link UnownedUserDataHost}, and 86 * stores the host as a {@link WeakReference} to be able to detach from it later. 87 * 88 * @param host The host to attach the {@code object} to. 89 * @param object The object to attach. 90 */ attachToHost(@onNull UnownedUserDataHost host, @NonNull T object)91 public final void attachToHost(@NonNull UnownedUserDataHost host, @NonNull T object) { 92 Objects.requireNonNull(object); 93 // Setting a new value might lead to detachment of previously attached data, including 94 // re-entry to this key, to happen before we update the {@link #mHostAttachments}. 95 host.set(this, object); 96 97 if (!isAttachedToHost(host)) { 98 mWeakHostAttachments.add(host); 99 } 100 } 101 102 /** 103 * Attempts to retrieve the instance of the {@link UnownedUserData} from the given {@link 104 * UnownedUserDataHost}. It will return {@code null} if the object is not attached to that 105 * particular {@link UnownedUserDataHost} using this key, or the {@link UnownedUserData} has 106 * been garbage collected. 107 * 108 * @param host The host to retrieve the {@link UnownedUserData} from. 109 * @return The current {@link UnownedUserData} stored in the {@code host}, or {@code null}. 110 */ 111 @Nullable retrieveDataFromHost(@onNull UnownedUserDataHost host)112 public final T retrieveDataFromHost(@NonNull UnownedUserDataHost host) { 113 assertNoDestroyedAttachments(); 114 for (UnownedUserDataHost attachedHost : mWeakHostAttachments) { 115 if (host.equals(attachedHost)) { 116 return host.get(this); 117 } 118 } 119 return null; 120 } 121 122 /** 123 * Detaches the key and object from the given host if it is attached with this key. It is OK to 124 * call this for already detached objects. 125 * 126 * @param host The host to detach from. 127 */ detachFromHost(@onNull UnownedUserDataHost host)128 public final void detachFromHost(@NonNull UnownedUserDataHost host) { 129 assertNoDestroyedAttachments(); 130 for (UnownedUserDataHost attachedHost : new ArrayList<>(mWeakHostAttachments)) { 131 if (host.equals(attachedHost)) { 132 removeHostAttachment(attachedHost); 133 } 134 } 135 } 136 137 /** 138 * Detaches the {@link UnownedUserData} from all hosts that it is currently attached to with 139 * this key. It is OK to call this for already detached objects. 140 * 141 * @param object The object to detach from all hosts. 142 */ detachFromAllHosts(@onNull T object)143 public final void detachFromAllHosts(@NonNull T object) { 144 assertNoDestroyedAttachments(); 145 for (UnownedUserDataHost attachedHost : new ArrayList<>(mWeakHostAttachments)) { 146 if (object.equals(attachedHost.get(this))) { 147 removeHostAttachment(attachedHost); 148 } 149 } 150 } 151 152 /** 153 * Checks if the {@link UnownedUserData} is currently attached to the given host with this key. 154 * 155 * @param host The host to check if the {@link UnownedUserData} is attached to. 156 * @return true if currently attached, false otherwise. 157 */ isAttachedToHost(@onNull UnownedUserDataHost host)158 public final boolean isAttachedToHost(@NonNull UnownedUserDataHost host) { 159 T t = retrieveDataFromHost(host); 160 return t != null; 161 } 162 163 /** 164 * @return Whether the {@link UnownedUserData} is currently attached to any hosts with this key. 165 */ isAttachedToAnyHost(@onNull T object)166 public final boolean isAttachedToAnyHost(@NonNull T object) { 167 return getHostAttachmentCount(object) > 0; 168 } 169 170 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) getHostAttachmentCount(@onNull T object)171 /* package */ int getHostAttachmentCount(@NonNull T object) { 172 assertNoDestroyedAttachments(); 173 int ret = 0; 174 for (UnownedUserDataHost attachedHost : mWeakHostAttachments) { 175 if (object.equals(attachedHost.get(this))) { 176 ret++; 177 } 178 } 179 return ret; 180 } 181 removeHostAttachment(UnownedUserDataHost host)182 private void removeHostAttachment(UnownedUserDataHost host) { 183 host.remove(this); 184 mWeakHostAttachments.remove(host); 185 } 186 assertNoDestroyedAttachments()187 private void assertNoDestroyedAttachments() { 188 if (BuildConfig.ENABLE_ASSERTS) { 189 for (UnownedUserDataHost attachedHost : mWeakHostAttachments) { 190 if (attachedHost.isDestroyed()) { 191 assert false : "Host should have been removed already."; 192 throw new IllegalStateException(); 193 } 194 } 195 } 196 } 197 } 198