• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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