Commit 0ed6308f authored by Jinsuk Kim's avatar Jinsuk Kim Committed by Commit Bot

Android: Generic Java UserData facility

Java UserDataHost/UserData is modeled after native C++
SupportsUserData/Data class to allow users to attach random class/data
to a 'host' class by key. Considering the limitation of Java disallowing
multiple inheritance, UserData itself is designed as interface, and
UserDataHost a final class that a host class defines as member variable.

Host class Foo exposes UserDataHost so that the classes to be attached to
Foo can be managed without Foo actually knowing about them. An object of
FooBar can be retrieved from UserHostData of Foo by:

 public class Foo {
     // Defines the container.
     private final UserDataHost mUserDataHost = new UserDataHost();

     public UserDataHost getUserDataHost() {
         return mUserDataHost;
     }
 }

 public class FooBar implements UserDataHost.UserData {

     public FooBar from(UserDataHost host) {
         FooBar foobar = host.getUserData(FooBar.class);
         // Instantiate FooBar upon the first access.
         return foobar != null ? foobar : host.setUserData(FooBar.class, new FooBar());
     }
 }

     Foo foo = new Foo();
     ...

     FooBar bar = FooBar.from(foo.getUserDataHost());

     ...

The design doc is here: https://goo.gl/2uWDja

The method |destroy| is defined in Userdata interface to have the lifecycle
entirely under control. Note that it is not *yet* possible to make it
work for WebView.

Change-Id: Iaae038a2b6577b07328917724c610c4efda9bb2e
Reviewed-on: https://chromium-review.googlesource.com/1179507Reviewed-by: default avatarTommy Nyquist <nyquist@chromium.org>
Commit-Queue: Jinsuk Kim <jinsukkim@chromium.org>
Cr-Commit-Position: refs/heads/master@{#586174}
parent 4819fdbc
...@@ -2929,6 +2929,8 @@ if (is_android) { ...@@ -2929,6 +2929,8 @@ if (is_android) {
"android/java/src/org/chromium/base/TimezoneUtils.java", "android/java/src/org/chromium/base/TimezoneUtils.java",
"android/java/src/org/chromium/base/TraceEvent.java", "android/java/src/org/chromium/base/TraceEvent.java",
"android/java/src/org/chromium/base/UnguessableToken.java", "android/java/src/org/chromium/base/UnguessableToken.java",
"android/java/src/org/chromium/base/UserData.java",
"android/java/src/org/chromium/base/UserDataHost.java",
"android/java/src/org/chromium/base/VisibleForTesting.java", "android/java/src/org/chromium/base/VisibleForTesting.java",
"android/java/src/org/chromium/base/annotations/AccessedByNative.java", "android/java/src/org/chromium/base/annotations/AccessedByNative.java",
"android/java/src/org/chromium/base/annotations/CalledByNative.java", "android/java/src/org/chromium/base/annotations/CalledByNative.java",
...@@ -2999,6 +3001,7 @@ if (is_android) { ...@@ -2999,6 +3001,7 @@ if (is_android) {
"android/javatests/src/org/chromium/base/CommandLineInitUtilTest.java", "android/javatests/src/org/chromium/base/CommandLineInitUtilTest.java",
"android/javatests/src/org/chromium/base/CommandLineTest.java", "android/javatests/src/org/chromium/base/CommandLineTest.java",
"android/javatests/src/org/chromium/base/EarlyTraceEventTest.java", "android/javatests/src/org/chromium/base/EarlyTraceEventTest.java",
"android/javatests/src/org/chromium/base/UserDataHostTest.java",
# TODO(nona): move to Junit once that is built for Android N. # TODO(nona): move to Junit once that is built for Android N.
"android/javatests/src/org/chromium/base/LocaleUtilsTest.java", "android/javatests/src/org/chromium/base/LocaleUtilsTest.java",
......
// Copyright 2018 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.base;
/**
* Interface to be implemented by the classes make themselves attacheable to
* a host class that holds {@link UserDataHost}.
*/
public interface UserData {
/**
* Called when {@link UserData} object needs to be destroyed.
* WARNING: This method is not guaranteed to be called. Each host class should
* call {@link UserDataHost#destroy()} explicitly at the end of its
* lifetime to have all of its {@link UserData#destroy()} get invoked.
*/
default void
destroy() {}
}
// Copyright 2018 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.base;
import android.os.Process;
import java.util.HashMap;
/**
* A class that implements type-safe heterogeneous container. It can associate
* an object of type T with a type token (T.class) as a key. Mismatch of the
* type between them can be checked at compile time, hence type-safe. Objects
* are held using strong reference in the container. {@code null} is not allowed
* for key or object.
* <p>
* Can be used for an object that needs to have other objects attached to it
* without having to manage explicit references to them. Attached objects need
* to implement {@link UserData} so that they can be destroyed by {@link #destroy()}.
* <p>
* No operation takes effect once {@link #destroy()} is called.
* <p>
* Usage:
*
* <code>
* public class Foo {
* // Defines the container.
* private final UserDataHost mUserDataHost = new UserDataHost();
*
* public UserDataHost getUserDataHost() {
* return mUserDataHost;
* }
* }
*
* public class FooBar implements UserData {
*
* public FooBar from(UserDataHost host) {
* FooBar foobar = host.getUserData(FooBar.class);
* // Instantiate FooBar upon the first access.
* return foobar != null ? foobar : host.setUserData(FooBar.class, new FooBar());
* }
* }
*
* Foo foo = new Foo();
* ...
*
* FooBar bar = FooBar.from(foo.getUserDataHost());
*
* ...
*
* </code>
*/
public final class UserDataHost {
private final long mThreadId = Process.myTid();
private HashMap<Class<? extends UserData>, UserData> mUserDataMap = new HashMap<>();
private void checkThreadAndState() {
assert mThreadId == Process.myTid() : "UserData must only be used on a single thread.";
assert mUserDataMap != null : "Operation is not allowed after destroy()";
}
/**
* Associates the specified object with the specified key.
* @param key Type token with which the specified object is to be associated.
* @param object Object to be associated with the specified key.
* @return the object just stored, or {@code null} if storing the object failed.
*/
public <T extends UserData> T setUserData(Class<T> key, T object) {
checkThreadAndState();
assert key != null && object != null : "Neither key nor object of UserDataHost can be null";
mUserDataMap.put(key, object);
return getUserData(key);
}
/**
* Returns the value to which the specified key is mapped, or null if this map
* contains no mapping for the key.
* @param key Type token for which the specified object is to be returned.
* @return the value to which the specified key is mapped, or null if this map
* contains no mapping for {@code key}.
*/
public <T extends UserData> T getUserData(Class<T> key) {
checkThreadAndState();
assert key != null : "UserDataHost key cannot be null";
return key.cast(mUserDataMap.get(key));
}
/**
* Removes the mapping for a key from this map. Assertion will be thrown if
* the given key has no mapping.
* @param key Type token for which the specified object is to be removed.
* @return The previous value associated with {@code key}.
*/
public <T extends UserData> T removeUserData(Class<T> key) {
checkThreadAndState();
assert key != null : "UserDataHost key cannot be null";
assert mUserDataMap.containsKey(key) : "UserData for the key is not present";
return key.cast(mUserDataMap.remove(key));
}
/**
* Destroy all the managed {@link UserData} instances. This should be invoked at
* the end of the lifetime of the host that user data instances hang on to.
* The host stops managing them after this method is called.
*/
public void destroy() {
checkThreadAndState();
// Nulls out |mUserDataMap| first in order to prevent concurrent modification that
// might happen in the for loop below.
HashMap<Class<? extends UserData>, UserData> map = mUserDataMap;
mUserDataMap = null;
for (UserData userData : map.values()) userData.destroy();
}
}
// Copyright 2018 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.base;
import android.support.test.filters.SmallTest;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.test.BaseJUnit4ClassRunner;
/**
* Test class for {@link UserDataHost}.
*/
@RunWith(BaseJUnit4ClassRunner.class)
public class UserDataHostTest {
private final UserDataHost mHost = new UserDataHost();
private static class TestObjectA implements UserData {
private boolean mDestroyed;
@Override
public void destroy() {
mDestroyed = true;
}
private boolean isDestroyed() {
return mDestroyed;
}
}
private static class TestObjectB implements UserData {
private boolean mDestroyed;
@Override
public void destroy() {
mDestroyed = true;
}
private boolean isDestroyed() {
return mDestroyed;
}
}
private <T extends UserData> void assertGetUserData(Class<T> key) {
boolean exception = false;
try {
mHost.getUserData(key);
} catch (AssertionError e) {
exception = true;
}
Assert.assertTrue(exception);
}
private <T extends UserData> void assertSetUserData(Class<T> key, T obj) {
boolean exception = false;
try {
mHost.setUserData(key, obj);
} catch (AssertionError e) {
exception = true;
}
Assert.assertTrue(exception);
}
private <T extends UserData> void assertRemoveUserData(Class<T> key) {
boolean exception = false;
try {
mHost.removeUserData(key);
} catch (AssertionError e) {
exception = true;
}
Assert.assertTrue(exception);
}
/**
* Verifies basic operations.
*/
@Test
@SmallTest
public void testBasicOperations() {
TestObjectA obj = new TestObjectA();
mHost.setUserData(TestObjectA.class, obj);
Assert.assertEquals(obj, mHost.getUserData(TestObjectA.class));
Assert.assertEquals(obj, mHost.removeUserData(TestObjectA.class));
Assert.assertNull(mHost.getUserData(TestObjectA.class));
assertRemoveUserData(TestObjectA.class);
}
/**
* Verifies nulled key or data are not allowed.
*/
@Test
@SmallTest
public void testNullKeyOrDataAreDisallowed() {
TestObjectA obj = new TestObjectA();
assertSetUserData(null, null);
assertSetUserData(TestObjectA.class, null);
assertSetUserData(null, obj);
assertGetUserData(null);
assertRemoveUserData(null);
}
/**
* Verifies {@link #setUserData()} overwrites current data.
*/
@Test
@SmallTest
public void testSetUserDataOverwrites() {
TestObjectA obj1 = new TestObjectA();
mHost.setUserData(TestObjectA.class, obj1);
Assert.assertEquals(obj1, mHost.getUserData(TestObjectA.class));
TestObjectA obj2 = new TestObjectA();
mHost.setUserData(TestObjectA.class, obj2);
Assert.assertEquals(obj2, mHost.getUserData(TestObjectA.class));
}
/**
* Verifies operation on a different thread is not allowed.
*/
@Test
@SmallTest
public void testSingleThreadPolicy() {
TestObjectA obj = new TestObjectA();
mHost.setUserData(TestObjectA.class, obj);
ThreadUtils.runOnUiThreadBlocking(() -> assertGetUserData(TestObjectA.class));
}
/**
* Verifies {@link UserHostData#destroy()} detroyes each {@link UserData} object.
*/
@Test
@SmallTest
public void testDestroy() {
TestObjectA objA = new TestObjectA();
TestObjectB objB = new TestObjectB();
mHost.setUserData(TestObjectA.class, objA);
mHost.setUserData(TestObjectB.class, objB);
Assert.assertEquals(objA, mHost.getUserData(TestObjectA.class));
Assert.assertEquals(objB, mHost.getUserData(TestObjectB.class));
mHost.destroy();
Assert.assertTrue(objA.isDestroyed());
Assert.assertTrue(objB.isDestroyed());
}
/**
* Verifies that no operation is allowed after {@link #destroy()} is called.
*/
@Test
@SmallTest
public void testOperationsDisallowedAfterDestroy() {
TestObjectA obj = new TestObjectA();
mHost.setUserData(TestObjectA.class, obj);
Assert.assertEquals(obj, mHost.getUserData(TestObjectA.class));
mHost.destroy();
assertGetUserData(TestObjectA.class);
assertSetUserData(TestObjectA.class, obj);
assertRemoveUserData(TestObjectA.class);
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment