Import Android SDK Platform P [4386628]
/google/data/ro/projects/android/fetch_artifact \
--bid 4386628 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4386628.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: I9b8400ac92116cae4f033d173f7a5682b26ccba9
diff --git a/android/arch/core/executor/AppToolkitTaskExecutor.java b/android/arch/core/executor/ArchTaskExecutor.java
similarity index 88%
rename from android/arch/core/executor/AppToolkitTaskExecutor.java
rename to android/arch/core/executor/ArchTaskExecutor.java
index 7337f74..2401a73 100644
--- a/android/arch/core/executor/AppToolkitTaskExecutor.java
+++ b/android/arch/core/executor/ArchTaskExecutor.java
@@ -29,8 +29,8 @@
* @hide This API is not final.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class AppToolkitTaskExecutor extends TaskExecutor {
- private static volatile AppToolkitTaskExecutor sInstance;
+public class ArchTaskExecutor extends TaskExecutor {
+ private static volatile ArchTaskExecutor sInstance;
@NonNull
private TaskExecutor mDelegate;
@@ -54,7 +54,7 @@
}
};
- private AppToolkitTaskExecutor() {
+ private ArchTaskExecutor() {
mDefaultTaskExecutor = new DefaultTaskExecutor();
mDelegate = mDefaultTaskExecutor;
}
@@ -62,15 +62,15 @@
/**
* Returns an instance of the task executor.
*
- * @return The singleton AppToolkitTaskExecutor.
+ * @return The singleton ArchTaskExecutor.
*/
- public static AppToolkitTaskExecutor getInstance() {
+ public static ArchTaskExecutor getInstance() {
if (sInstance != null) {
return sInstance;
}
- synchronized (AppToolkitTaskExecutor.class) {
+ synchronized (ArchTaskExecutor.class) {
if (sInstance == null) {
- sInstance = new AppToolkitTaskExecutor();
+ sInstance = new ArchTaskExecutor();
}
}
return sInstance;
diff --git a/android/arch/core/executor/JunitTaskExecutorRule.java b/android/arch/core/executor/JunitTaskExecutorRule.java
index cd4f8f5..c3366f3 100644
--- a/android/arch/core/executor/JunitTaskExecutorRule.java
+++ b/android/arch/core/executor/JunitTaskExecutorRule.java
@@ -46,11 +46,11 @@
}
private void beforeStart() {
- AppToolkitTaskExecutor.getInstance().setDelegate(mTaskExecutor);
+ ArchTaskExecutor.getInstance().setDelegate(mTaskExecutor);
}
private void afterFinished() {
- AppToolkitTaskExecutor.getInstance().setDelegate(null);
+ ArchTaskExecutor.getInstance().setDelegate(null);
}
public TaskExecutor getTaskExecutor() {
diff --git a/android/arch/core/executor/testing/CountingTaskExecutorRule.java b/android/arch/core/executor/testing/CountingTaskExecutorRule.java
index ad930aa..77133d5 100644
--- a/android/arch/core/executor/testing/CountingTaskExecutorRule.java
+++ b/android/arch/core/executor/testing/CountingTaskExecutorRule.java
@@ -16,7 +16,7 @@
package android.arch.core.executor.testing;
-import android.arch.core.executor.AppToolkitTaskExecutor;
+import android.arch.core.executor.ArchTaskExecutor;
import android.arch.core.executor.DefaultTaskExecutor;
import android.os.SystemClock;
@@ -39,7 +39,7 @@
@Override
protected void starting(Description description) {
super.starting(description);
- AppToolkitTaskExecutor.getInstance().setDelegate(new DefaultTaskExecutor() {
+ ArchTaskExecutor.getInstance().setDelegate(new DefaultTaskExecutor() {
@Override
public void executeOnDiskIO(Runnable runnable) {
super.executeOnDiskIO(new CountingRunnable(runnable));
@@ -55,7 +55,7 @@
@Override
protected void finished(Description description) {
super.finished(description);
- AppToolkitTaskExecutor.getInstance().setDelegate(null);
+ ArchTaskExecutor.getInstance().setDelegate(null);
}
private void increment() {
diff --git a/android/arch/core/executor/testing/CountingTaskExecutorRuleTest.java b/android/arch/core/executor/testing/CountingTaskExecutorRuleTest.java
index ad36b9b..a6a5b2e 100644
--- a/android/arch/core/executor/testing/CountingTaskExecutorRuleTest.java
+++ b/android/arch/core/executor/testing/CountingTaskExecutorRuleTest.java
@@ -19,7 +19,7 @@
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
-import android.arch.core.executor.AppToolkitTaskExecutor;
+import android.arch.core.executor.ArchTaskExecutor;
import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4;
@@ -115,13 +115,13 @@
private LatchRunnable runOnIO() {
LatchRunnable latchRunnable = new LatchRunnable();
- AppToolkitTaskExecutor.getInstance().executeOnDiskIO(latchRunnable);
+ ArchTaskExecutor.getInstance().executeOnDiskIO(latchRunnable);
return latchRunnable;
}
private LatchRunnable runOnMain() {
LatchRunnable latchRunnable = new LatchRunnable();
- AppToolkitTaskExecutor.getInstance().executeOnMainThread(latchRunnable);
+ ArchTaskExecutor.getInstance().executeOnMainThread(latchRunnable);
return latchRunnable;
}
diff --git a/android/arch/core/executor/testing/InstantTaskExecutorRule.java b/android/arch/core/executor/testing/InstantTaskExecutorRule.java
index 07dcf1f..f88a3e3 100644
--- a/android/arch/core/executor/testing/InstantTaskExecutorRule.java
+++ b/android/arch/core/executor/testing/InstantTaskExecutorRule.java
@@ -16,7 +16,7 @@
package android.arch.core.executor.testing;
-import android.arch.core.executor.AppToolkitTaskExecutor;
+import android.arch.core.executor.ArchTaskExecutor;
import android.arch.core.executor.TaskExecutor;
import org.junit.rules.TestWatcher;
@@ -32,7 +32,7 @@
@Override
protected void starting(Description description) {
super.starting(description);
- AppToolkitTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
+ ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
@Override
public void executeOnDiskIO(Runnable runnable) {
runnable.run();
@@ -53,6 +53,6 @@
@Override
protected void finished(Description description) {
super.finished(description);
- AppToolkitTaskExecutor.getInstance().setDelegate(null);
+ ArchTaskExecutor.getInstance().setDelegate(null);
}
}
diff --git a/android/arch/core/executor/testing/InstantTaskExecutorRuleTest.java b/android/arch/core/executor/testing/InstantTaskExecutorRuleTest.java
index 4345fd1..0fdcbfb 100644
--- a/android/arch/core/executor/testing/InstantTaskExecutorRuleTest.java
+++ b/android/arch/core/executor/testing/InstantTaskExecutorRuleTest.java
@@ -18,7 +18,7 @@
import static org.junit.Assert.assertTrue;
-import android.arch.core.executor.AppToolkitTaskExecutor;
+import android.arch.core.executor.ArchTaskExecutor;
import org.junit.Rule;
import org.junit.Test;
@@ -46,7 +46,7 @@
return null;
}
});
- AppToolkitTaskExecutor.getInstance().executeOnMainThread(check);
+ ArchTaskExecutor.getInstance().executeOnMainThread(check);
check.get(1, TimeUnit.SECONDS);
}
@@ -60,7 +60,7 @@
return null;
}
});
- AppToolkitTaskExecutor.getInstance().executeOnDiskIO(check);
+ ArchTaskExecutor.getInstance().executeOnDiskIO(check);
check.get(1, TimeUnit.SECONDS);
}
}
diff --git a/android/arch/lifecycle/ClassesInfoCache.java b/android/arch/lifecycle/ClassesInfoCache.java
new file mode 100644
index 0000000..f077dae
--- /dev/null
+++ b/android/arch/lifecycle/ClassesInfoCache.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle;
+
+import android.support.annotation.Nullable;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Reflection is expensive, so we cache information about methods
+ * for {@link ReflectiveGenericLifecycleObserver}, so it can call them,
+ * and for {@link Lifecycling} to determine which observer adapter to use.
+ */
+class ClassesInfoCache {
+
+ static ClassesInfoCache sInstance = new ClassesInfoCache();
+
+ private static final int CALL_TYPE_NO_ARG = 0;
+ private static final int CALL_TYPE_PROVIDER = 1;
+ private static final int CALL_TYPE_PROVIDER_WITH_EVENT = 2;
+
+ private final Map<Class, CallbackInfo> mCallbackMap = new HashMap<>();
+ private final Map<Class, Boolean> mHasLifecycleMethods = new HashMap<>();
+
+ boolean hasLifecycleMethods(Class klass) {
+ if (mHasLifecycleMethods.containsKey(klass)) {
+ return mHasLifecycleMethods.get(klass);
+ }
+
+ Method[] methods = klass.getDeclaredMethods();
+ for (Method method : methods) {
+ OnLifecycleEvent annotation = method.getAnnotation(OnLifecycleEvent.class);
+ if (annotation != null) {
+ // Optimization for reflection, we know that this method is called
+ // when there is no generated adapter. But there are methods with @OnLifecycleEvent
+ // so we know that will use ReflectiveGenericLifecycleObserver,
+ // so we createInfo in advance.
+ // CreateInfo always initialize mHasLifecycleMethods for a class, so we don't do it
+ // here.
+ createInfo(klass, methods);
+ return true;
+ }
+ }
+ mHasLifecycleMethods.put(klass, false);
+ return false;
+ }
+
+ CallbackInfo getInfo(Class klass) {
+ CallbackInfo existing = mCallbackMap.get(klass);
+ if (existing != null) {
+ return existing;
+ }
+ existing = createInfo(klass, null);
+ return existing;
+ }
+
+ private void verifyAndPutHandler(Map<MethodReference, Lifecycle.Event> handlers,
+ MethodReference newHandler, Lifecycle.Event newEvent, Class klass) {
+ Lifecycle.Event event = handlers.get(newHandler);
+ if (event != null && newEvent != event) {
+ Method method = newHandler.mMethod;
+ throw new IllegalArgumentException(
+ "Method " + method.getName() + " in " + klass.getName()
+ + " already declared with different @OnLifecycleEvent value: previous"
+ + " value " + event + ", new value " + newEvent);
+ }
+ if (event == null) {
+ handlers.put(newHandler, newEvent);
+ }
+ }
+
+ private CallbackInfo createInfo(Class klass, @Nullable Method[] declaredMethods) {
+ Class superclass = klass.getSuperclass();
+ Map<MethodReference, Lifecycle.Event> handlerToEvent = new HashMap<>();
+ if (superclass != null) {
+ CallbackInfo superInfo = getInfo(superclass);
+ if (superInfo != null) {
+ handlerToEvent.putAll(superInfo.mHandlerToEvent);
+ }
+ }
+
+ Class[] interfaces = klass.getInterfaces();
+ for (Class intrfc : interfaces) {
+ for (Map.Entry<MethodReference, Lifecycle.Event> entry : getInfo(
+ intrfc).mHandlerToEvent.entrySet()) {
+ verifyAndPutHandler(handlerToEvent, entry.getKey(), entry.getValue(), klass);
+ }
+ }
+
+ Method[] methods = declaredMethods != null ? declaredMethods : klass.getDeclaredMethods();
+ boolean hasLifecycleMethods = false;
+ for (Method method : methods) {
+ OnLifecycleEvent annotation = method.getAnnotation(OnLifecycleEvent.class);
+ if (annotation == null) {
+ continue;
+ }
+ hasLifecycleMethods = true;
+ Class<?>[] params = method.getParameterTypes();
+ int callType = CALL_TYPE_NO_ARG;
+ if (params.length > 0) {
+ callType = CALL_TYPE_PROVIDER;
+ if (!params[0].isAssignableFrom(LifecycleOwner.class)) {
+ throw new IllegalArgumentException(
+ "invalid parameter type. Must be one and instanceof LifecycleOwner");
+ }
+ }
+ Lifecycle.Event event = annotation.value();
+
+ if (params.length > 1) {
+ callType = CALL_TYPE_PROVIDER_WITH_EVENT;
+ if (!params[1].isAssignableFrom(Lifecycle.Event.class)) {
+ throw new IllegalArgumentException(
+ "invalid parameter type. second arg must be an event");
+ }
+ if (event != Lifecycle.Event.ON_ANY) {
+ throw new IllegalArgumentException(
+ "Second arg is supported only for ON_ANY value");
+ }
+ }
+ if (params.length > 2) {
+ throw new IllegalArgumentException("cannot have more than 2 params");
+ }
+ MethodReference methodReference = new MethodReference(callType, method);
+ verifyAndPutHandler(handlerToEvent, methodReference, event, klass);
+ }
+ CallbackInfo info = new CallbackInfo(handlerToEvent);
+ mCallbackMap.put(klass, info);
+ mHasLifecycleMethods.put(klass, hasLifecycleMethods);
+ return info;
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ static class CallbackInfo {
+ final Map<Lifecycle.Event, List<MethodReference>> mEventToHandlers;
+ final Map<MethodReference, Lifecycle.Event> mHandlerToEvent;
+
+ CallbackInfo(Map<MethodReference, Lifecycle.Event> handlerToEvent) {
+ mHandlerToEvent = handlerToEvent;
+ mEventToHandlers = new HashMap<>();
+ for (Map.Entry<MethodReference, Lifecycle.Event> entry : handlerToEvent.entrySet()) {
+ Lifecycle.Event event = entry.getValue();
+ List<MethodReference> methodReferences = mEventToHandlers.get(event);
+ if (methodReferences == null) {
+ methodReferences = new ArrayList<>();
+ mEventToHandlers.put(event, methodReferences);
+ }
+ methodReferences.add(entry.getKey());
+ }
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ void invokeCallbacks(LifecycleOwner source, Lifecycle.Event event, Object target) {
+ invokeMethodsForEvent(mEventToHandlers.get(event), source, event, target);
+ invokeMethodsForEvent(mEventToHandlers.get(Lifecycle.Event.ON_ANY), source, event,
+ target);
+ }
+
+ private static void invokeMethodsForEvent(List<MethodReference> handlers,
+ LifecycleOwner source, Lifecycle.Event event, Object mWrapped) {
+ if (handlers != null) {
+ for (int i = handlers.size() - 1; i >= 0; i--) {
+ handlers.get(i).invokeCallback(source, event, mWrapped);
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ static class MethodReference {
+ final int mCallType;
+ final Method mMethod;
+
+ MethodReference(int callType, Method method) {
+ mCallType = callType;
+ mMethod = method;
+ mMethod.setAccessible(true);
+ }
+
+ void invokeCallback(LifecycleOwner source, Lifecycle.Event event, Object target) {
+ //noinspection TryWithIdenticalCatches
+ try {
+ switch (mCallType) {
+ case CALL_TYPE_NO_ARG:
+ mMethod.invoke(target);
+ break;
+ case CALL_TYPE_PROVIDER:
+ mMethod.invoke(target, source);
+ break;
+ case CALL_TYPE_PROVIDER_WITH_EVENT:
+ mMethod.invoke(target, source, event);
+ break;
+ }
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException("Failed to call observer method", e.getCause());
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ MethodReference that = (MethodReference) o;
+ return mCallType == that.mCallType && mMethod.getName().equals(that.mMethod.getName());
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * mCallType + mMethod.getName().hashCode();
+ }
+ }
+}
diff --git a/android/arch/lifecycle/CompositeGeneratedAdaptersObserver.java b/android/arch/lifecycle/CompositeGeneratedAdaptersObserver.java
new file mode 100644
index 0000000..e8cbe7c
--- /dev/null
+++ b/android/arch/lifecycle/CompositeGeneratedAdaptersObserver.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle;
+
+
+import android.support.annotation.RestrictTo;
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class CompositeGeneratedAdaptersObserver implements GenericLifecycleObserver {
+
+ private final GeneratedAdapter[] mGeneratedAdapters;
+
+ CompositeGeneratedAdaptersObserver(GeneratedAdapter[] generatedAdapters) {
+ mGeneratedAdapters = generatedAdapters;
+ }
+
+ @Override
+ public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
+ MethodCallsLogger logger = new MethodCallsLogger();
+ for (GeneratedAdapter mGenerated: mGeneratedAdapters) {
+ mGenerated.callMethods(source, event, false, logger);
+ }
+ for (GeneratedAdapter mGenerated: mGeneratedAdapters) {
+ mGenerated.callMethods(source, event, true, logger);
+ }
+ }
+}
diff --git a/android/arch/lifecycle/ComputableLiveData.java b/android/arch/lifecycle/ComputableLiveData.java
index fe18243..f135244 100644
--- a/android/arch/lifecycle/ComputableLiveData.java
+++ b/android/arch/lifecycle/ComputableLiveData.java
@@ -1,136 +1,9 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
+//ComputableLiveData interface for tests
package android.arch.lifecycle;
-
-import android.arch.core.executor.AppToolkitTaskExecutor;
-import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.VisibleForTesting;
-import android.support.annotation.WorkerThread;
-
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * A LiveData class that can be invalidated & computed on demand.
- * <p>
- * This is an internal class for now, might be public if we see the necessity.
- *
- * @param <T> The type of the live data
- * @hide internal
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+import android.arch.lifecycle.LiveData;
public abstract class ComputableLiveData<T> {
-
- private final LiveData<T> mLiveData;
-
- private AtomicBoolean mInvalid = new AtomicBoolean(true);
- private AtomicBoolean mComputing = new AtomicBoolean(false);
-
- /**
- * Creates a computable live data which is computed when there are active observers.
- * <p>
- * It can also be invalidated via {@link #invalidate()} which will result in a call to
- * {@link #compute()} if there are active observers (or when they start observing)
- */
- @SuppressWarnings("WeakerAccess")
- public ComputableLiveData() {
- mLiveData = new LiveData<T>() {
- @Override
- protected void onActive() {
- // TODO if we make this class public, we should accept an executor
- AppToolkitTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable);
- }
- };
- }
-
- /**
- * Returns the LiveData managed by this class.
- *
- * @return A LiveData that is controlled by ComputableLiveData.
- */
- @SuppressWarnings("WeakerAccess")
- @NonNull
- public LiveData<T> getLiveData() {
- return mLiveData;
- }
-
- @VisibleForTesting
- final Runnable mRefreshRunnable = new Runnable() {
- @WorkerThread
- @Override
- public void run() {
- boolean computed;
- do {
- computed = false;
- // compute can happen only in 1 thread but no reason to lock others.
- if (mComputing.compareAndSet(false, true)) {
- // as long as it is invalid, keep computing.
- try {
- T value = null;
- while (mInvalid.compareAndSet(true, false)) {
- computed = true;
- value = compute();
- }
- if (computed) {
- mLiveData.postValue(value);
- }
- } finally {
- // release compute lock
- mComputing.set(false);
- }
- }
- // check invalid after releasing compute lock to avoid the following scenario.
- // Thread A runs compute()
- // Thread A checks invalid, it is false
- // Main thread sets invalid to true
- // Thread B runs, fails to acquire compute lock and skips
- // Thread A releases compute lock
- // We've left invalid in set state. The check below recovers.
- } while (computed && mInvalid.get());
- }
- };
-
- // invalidation check always happens on the main thread
- @VisibleForTesting
- final Runnable mInvalidationRunnable = new Runnable() {
- @MainThread
- @Override
- public void run() {
- boolean isActive = mLiveData.hasActiveObservers();
- if (mInvalid.compareAndSet(false, true)) {
- if (isActive) {
- // TODO if we make this class public, we should accept an executor.
- AppToolkitTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable);
- }
- }
- }
- };
-
- /**
- * Invalidates the LiveData.
- * <p>
- * When there are active observers, this will trigger a call to {@link #compute()}.
- */
- public void invalidate() {
- AppToolkitTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
- }
-
- @SuppressWarnings("WeakerAccess")
- @WorkerThread
- protected abstract T compute();
+ public ComputableLiveData(){}
+ abstract protected T compute();
+ public LiveData<T> getLiveData() {return null;}
+ public void invalidate() {}
}
diff --git a/android/arch/lifecycle/ComputableLiveDataTest.java b/android/arch/lifecycle/ComputableLiveDataTest.java
index 0a3fbed..eb89d8d 100644
--- a/android/arch/lifecycle/ComputableLiveDataTest.java
+++ b/android/arch/lifecycle/ComputableLiveDataTest.java
@@ -27,7 +27,7 @@
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
-import android.arch.core.executor.AppToolkitTaskExecutor;
+import android.arch.core.executor.ArchTaskExecutor;
import android.arch.core.executor.TaskExecutor;
import android.arch.core.executor.TaskExecutorWithFakeMainThread;
import android.arch.lifecycle.util.InstantTaskExecutor;
@@ -58,12 +58,12 @@
@Before
public void swapExecutorDelegate() {
mTaskExecutor = spy(new InstantTaskExecutor());
- AppToolkitTaskExecutor.getInstance().setDelegate(mTaskExecutor);
+ ArchTaskExecutor.getInstance().setDelegate(mTaskExecutor);
}
@After
public void removeExecutorDelegate() {
- AppToolkitTaskExecutor.getInstance().setDelegate(null);
+ ArchTaskExecutor.getInstance().setDelegate(null);
}
@Test
@@ -76,7 +76,7 @@
@Test
public void noConcurrentCompute() throws InterruptedException {
TaskExecutorWithFakeMainThread executor = new TaskExecutorWithFakeMainThread(2);
- AppToolkitTaskExecutor.getInstance().setDelegate(executor);
+ ArchTaskExecutor.getInstance().setDelegate(executor);
try {
// # of compute calls
final Semaphore computeCounter = new Semaphore(0);
@@ -121,7 +121,7 @@
// assert no other results arrive
verify(observer, never()).onChanged(anyInt());
} finally {
- AppToolkitTaskExecutor.getInstance().setDelegate(null);
+ ArchTaskExecutor.getInstance().setDelegate(null);
}
}
diff --git a/android/arch/lifecycle/DefaultLifecycleObserver.java b/android/arch/lifecycle/DefaultLifecycleObserver.java
new file mode 100644
index 0000000..b6f468c
--- /dev/null
+++ b/android/arch/lifecycle/DefaultLifecycleObserver.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle;
+
+import android.support.annotation.NonNull;
+
+/**
+ * Callback interface for listening to {@link LifecycleOwner} state changes.
+ * <p>
+ * If you use Java 8 language, <b>always</b> prefer it over annotations.
+ */
+@SuppressWarnings("unused")
+public interface DefaultLifecycleObserver extends FullLifecycleObserver {
+
+ /**
+ * Notifies that {@code ON_CREATE} event occurred.
+ * <p>
+ * This method will be called after the {@link LifecycleOwner}'s {@code onCreate}
+ * method returns.
+ *
+ * @param owner the component, whose state was changed
+ */
+ @Override
+ default void onCreate(@NonNull LifecycleOwner owner) {
+ }
+
+ /**
+ * Notifies that {@code ON_START} event occurred.
+ * <p>
+ * This method will be called after the {@link LifecycleOwner}'s {@code onStart} method returns.
+ *
+ * @param owner the component, whose state was changed
+ */
+ @Override
+ default void onStart(@NonNull LifecycleOwner owner) {
+ }
+
+ /**
+ * Notifies that {@code ON_RESUME} event occurred.
+ * <p>
+ * This method will be called after the {@link LifecycleOwner}'s {@code onResume}
+ * method returns.
+ *
+ * @param owner the component, whose state was changed
+ */
+ @Override
+ default void onResume(@NonNull LifecycleOwner owner) {
+ }
+
+ /**
+ * Notifies that {@code ON_PAUSE} event occurred.
+ * <p>
+ * This method will be called before the {@link LifecycleOwner}'s {@code onPause} method
+ * is called.
+ *
+ * @param owner the component, whose state was changed
+ */
+ @Override
+ default void onPause(@NonNull LifecycleOwner owner) {
+ }
+
+ /**
+ * Notifies that {@code ON_STOP} event occurred.
+ * <p>
+ * This method will be called before the {@link LifecycleOwner}'s {@code onStop} method
+ * is called.
+ *
+ * @param owner the component, whose state was changed
+ */
+ @Override
+ default void onStop(@NonNull LifecycleOwner owner) {
+ }
+
+ /**
+ * Notifies that {@code ON_DESTROY} event occurred.
+ * <p>
+ * This method will be called before the {@link LifecycleOwner}'s {@code onStop} method
+ * is called.
+ *
+ * @param owner the component, whose state was changed
+ */
+ @Override
+ default void onDestroy(@NonNull LifecycleOwner owner) {
+ }
+}
+
diff --git a/android/arch/lifecycle/FragmentInBackStackLifecycleTest.java b/android/arch/lifecycle/FragmentInBackStackLifecycleTest.java
index 3397f5f..f48f788 100644
--- a/android/arch/lifecycle/FragmentInBackStackLifecycleTest.java
+++ b/android/arch/lifecycle/FragmentInBackStackLifecycleTest.java
@@ -37,6 +37,7 @@
import android.support.test.filters.SmallTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
@@ -58,20 +59,20 @@
final ArrayList<Event> collectedEvents = new ArrayList<>();
LifecycleObserver collectingObserver = new LifecycleObserver() {
@OnLifecycleEvent(Event.ON_ANY)
- void onAny(LifecycleOwner owner, Event event) {
+ void onAny(@SuppressWarnings("unused") LifecycleOwner owner, Event event) {
collectedEvents.add(event);
}
};
final FragmentActivity activity = activityTestRule.getActivity();
activityTestRule.runOnUiThread(() -> {
FragmentManager fm = activity.getSupportFragmentManager();
- LifecycleFragment fragment = new LifecycleFragment();
+ Fragment fragment = new Fragment();
fm.beginTransaction().add(R.id.fragment_container, fragment, "tag").addToBackStack(null)
.commit();
fm.executePendingTransactions();
fragment.getLifecycle().addObserver(collectingObserver);
- LifecycleFragment fragment2 = new LifecycleFragment();
+ Fragment fragment2 = new Fragment();
fm.beginTransaction().replace(R.id.fragment_container, fragment2).addToBackStack(null)
.commit();
fm.executePendingTransactions();
@@ -82,12 +83,13 @@
EmptyActivity newActivity = recreateActivity(activityTestRule.getActivity(),
activityTestRule);
+ //noinspection ArraysAsListWithZeroOrOneArgument
assertThat(collectedEvents, is(asList(ON_DESTROY)));
collectedEvents.clear();
EmptyActivity lastActivity = recreateActivity(newActivity, activityTestRule);
activityTestRule.runOnUiThread(() -> {
FragmentManager fm = lastActivity.getSupportFragmentManager();
- LifecycleFragment fragment = (LifecycleFragment) fm.findFragmentByTag("tag");
+ Fragment fragment = fm.findFragmentByTag("tag");
fragment.getLifecycle().addObserver(collectingObserver);
assertThat(collectedEvents, iterableWithSize(0));
fm.popBackStackImmediate();
diff --git a/android/arch/lifecycle/FragmentOperationsLifecycleTest.java b/android/arch/lifecycle/FragmentOperationsLifecycleTest.java
index be062cb..3e61277 100644
--- a/android/arch/lifecycle/FragmentOperationsLifecycleTest.java
+++ b/android/arch/lifecycle/FragmentOperationsLifecycleTest.java
@@ -34,6 +34,7 @@
import android.support.test.filters.MediumTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import org.junit.Rule;
@@ -55,7 +56,7 @@
@UiThreadTest
public void addRemoveFragment() {
EmptyActivity activity = mActivityTestRule.getActivity();
- LifecycleFragment fragment = new LifecycleFragment();
+ Fragment fragment = new Fragment();
FragmentManager fm = activity.getSupportFragmentManager();
fm.beginTransaction().add(fragment, "tag").commitNow();
CollectingObserver observer = observeAndCollectIn(fragment);
@@ -70,7 +71,7 @@
@UiThreadTest
public void fragmentInBackstack() {
EmptyActivity activity = mActivityTestRule.getActivity();
- LifecycleFragment fragment1 = new LifecycleFragment();
+ Fragment fragment1 = new Fragment();
FragmentManager fm = activity.getSupportFragmentManager();
fm.beginTransaction().add(R.id.fragment_container, fragment1, "tag").addToBackStack(null)
.commit();
@@ -78,7 +79,7 @@
CollectingObserver observer1 = observeAndCollectIn(fragment1);
assertThat(observer1.getEventsAndReset(), is(asList(ON_CREATE, ON_START, ON_RESUME)));
- LifecycleFragment fragment2 = new LifecycleFragment();
+ Fragment fragment2 = new Fragment();
fm.beginTransaction().replace(R.id.fragment_container, fragment2).addToBackStack(null)
.commit();
fm.executePendingTransactions();
@@ -95,7 +96,7 @@
assertThat(observer1.getEventsAndReset(), is(asList(ON_PAUSE, ON_STOP, ON_DESTROY)));
}
- private static CollectingObserver observeAndCollectIn(LifecycleFragment fragment) {
+ private static CollectingObserver observeAndCollectIn(Fragment fragment) {
CollectingObserver observer = new CollectingObserver();
fragment.getLifecycle().addObserver(observer);
return observer;
diff --git a/android/arch/lifecycle/FullLifecycleObserver.java b/android/arch/lifecycle/FullLifecycleObserver.java
new file mode 100644
index 0000000..f179274
--- /dev/null
+++ b/android/arch/lifecycle/FullLifecycleObserver.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle;
+
+interface FullLifecycleObserver extends LifecycleObserver {
+
+ void onCreate(LifecycleOwner owner);
+
+ void onStart(LifecycleOwner owner);
+
+ void onResume(LifecycleOwner owner);
+
+ void onPause(LifecycleOwner owner);
+
+ void onStop(LifecycleOwner owner);
+
+ void onDestroy(LifecycleOwner owner);
+}
diff --git a/android/arch/lifecycle/FullLifecycleObserverAdapter.java b/android/arch/lifecycle/FullLifecycleObserverAdapter.java
new file mode 100644
index 0000000..0a91a66
--- /dev/null
+++ b/android/arch/lifecycle/FullLifecycleObserverAdapter.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle;
+
+class FullLifecycleObserverAdapter implements GenericLifecycleObserver {
+
+ private final FullLifecycleObserver mObserver;
+
+ FullLifecycleObserverAdapter(FullLifecycleObserver observer) {
+ mObserver = observer;
+ }
+
+ @Override
+ public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
+ switch (event) {
+ case ON_CREATE:
+ mObserver.onCreate(source);
+ break;
+ case ON_START:
+ mObserver.onStart(source);
+ break;
+ case ON_RESUME:
+ mObserver.onResume(source);
+ break;
+ case ON_PAUSE:
+ mObserver.onPause(source);
+ break;
+ case ON_STOP:
+ mObserver.onStop(source);
+ break;
+ case ON_DESTROY:
+ mObserver.onDestroy(source);
+ break;
+ case ON_ANY:
+ throw new IllegalArgumentException("ON_ANY must not been send by anybody");
+ }
+ }
+}
diff --git a/android/arch/lifecycle/FullLifecycleObserverTest.java b/android/arch/lifecycle/FullLifecycleObserverTest.java
new file mode 100644
index 0000000..def6755
--- /dev/null
+++ b/android/arch/lifecycle/FullLifecycleObserverTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle;
+
+import static android.arch.lifecycle.Lifecycle.Event.ON_CREATE;
+import static android.arch.lifecycle.Lifecycle.Event.ON_DESTROY;
+import static android.arch.lifecycle.Lifecycle.Event.ON_PAUSE;
+import static android.arch.lifecycle.Lifecycle.Event.ON_RESUME;
+import static android.arch.lifecycle.Lifecycle.Event.ON_START;
+import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static android.arch.lifecycle.Lifecycle.State.CREATED;
+import static android.arch.lifecycle.Lifecycle.State.INITIALIZED;
+import static android.arch.lifecycle.Lifecycle.State.RESUMED;
+import static android.arch.lifecycle.Lifecycle.State.STARTED;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.when;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+
+@RunWith(JUnit4.class)
+public class FullLifecycleObserverTest {
+ private LifecycleOwner mOwner;
+ private Lifecycle mLifecycle;
+
+ @Before
+ public void initMocks() {
+ mOwner = mock(LifecycleOwner.class);
+ mLifecycle = mock(Lifecycle.class);
+ when(mOwner.getLifecycle()).thenReturn(mLifecycle);
+ }
+
+ @Test
+ public void eachEvent() {
+ FullLifecycleObserver obj = mock(FullLifecycleObserver.class);
+ FullLifecycleObserverAdapter observer = new FullLifecycleObserverAdapter(obj);
+ when(mLifecycle.getCurrentState()).thenReturn(CREATED);
+
+ observer.onStateChanged(mOwner, ON_CREATE);
+ InOrder inOrder = Mockito.inOrder(obj);
+ inOrder.verify(obj).onCreate(mOwner);
+ reset(obj);
+
+ when(mLifecycle.getCurrentState()).thenReturn(STARTED);
+ observer.onStateChanged(mOwner, ON_START);
+ inOrder.verify(obj).onStart(mOwner);
+ reset(obj);
+
+ when(mLifecycle.getCurrentState()).thenReturn(RESUMED);
+ observer.onStateChanged(mOwner, ON_RESUME);
+ inOrder.verify(obj).onResume(mOwner);
+ reset(obj);
+
+ when(mLifecycle.getCurrentState()).thenReturn(STARTED);
+ observer.onStateChanged(mOwner, ON_PAUSE);
+ inOrder.verify(obj).onPause(mOwner);
+ reset(obj);
+
+ when(mLifecycle.getCurrentState()).thenReturn(CREATED);
+ observer.onStateChanged(mOwner, ON_STOP);
+ inOrder.verify(obj).onStop(mOwner);
+ reset(obj);
+
+ when(mLifecycle.getCurrentState()).thenReturn(INITIALIZED);
+ observer.onStateChanged(mOwner, ON_DESTROY);
+ inOrder.verify(obj).onDestroy(mOwner);
+ reset(obj);
+ }
+}
diff --git a/android/arch/lifecycle/GeneratedAdapter.java b/android/arch/lifecycle/GeneratedAdapter.java
new file mode 100644
index 0000000..a8862da
--- /dev/null
+++ b/android/arch/lifecycle/GeneratedAdapter.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle;
+
+import android.support.annotation.RestrictTo;
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface GeneratedAdapter {
+
+ /**
+ * Called when a state transition event happens.
+ *
+ * @param source The source of the event
+ * @param event The event
+ * @param onAny approveCall onAny handlers
+ * @param logger if passed, used to track called methods and prevent calling the same method
+ * twice
+ */
+ void callMethods(LifecycleOwner source, Lifecycle.Event event, boolean onAny,
+ MethodCallsLogger logger);
+}
diff --git a/android/arch/lifecycle/GeneratedAdaptersTest.java b/android/arch/lifecycle/GeneratedAdaptersTest.java
new file mode 100644
index 0000000..2abb511
--- /dev/null
+++ b/android/arch/lifecycle/GeneratedAdaptersTest.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle;
+
+import static android.arch.lifecycle.Lifecycle.Event.ON_ANY;
+import static android.arch.lifecycle.Lifecycle.Event.ON_RESUME;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class GeneratedAdaptersTest {
+
+ private LifecycleOwner mOwner;
+ @SuppressWarnings("FieldCanBeLocal")
+ private Lifecycle mLifecycle;
+
+ @Before
+ public void initMocks() {
+ mOwner = mock(LifecycleOwner.class);
+ mLifecycle = mock(Lifecycle.class);
+ when(mOwner.getLifecycle()).thenReturn(mLifecycle);
+ }
+
+ static class SimpleObserver implements LifecycleObserver {
+ List<String> mLog;
+
+ SimpleObserver(List<String> log) {
+ mLog = log;
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
+ void onCreate() {
+ mLog.add("onCreate");
+ }
+ }
+
+ @Test
+ public void testSimpleSingleGeneratedAdapter() {
+ List<String> actual = new ArrayList<>();
+ GenericLifecycleObserver callback = Lifecycling.getCallback(new SimpleObserver(actual));
+ callback.onStateChanged(mOwner, Lifecycle.Event.ON_CREATE);
+ assertThat(callback, instanceOf(SingleGeneratedAdapterObserver.class));
+ assertThat(actual, is(singletonList("onCreate")));
+ }
+
+ static class TestObserver implements LifecycleObserver {
+ List<String> mLog;
+
+ TestObserver(List<String> log) {
+ mLog = log;
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
+ void onCreate() {
+ mLog.add("onCreate");
+ }
+
+ @OnLifecycleEvent(ON_ANY)
+ void onAny() {
+ mLog.add("onAny");
+ }
+ }
+
+ @Test
+ public void testOnAny() {
+ List<String> actual = new ArrayList<>();
+ GenericLifecycleObserver callback = Lifecycling.getCallback(new TestObserver(actual));
+ callback.onStateChanged(mOwner, Lifecycle.Event.ON_CREATE);
+ assertThat(callback, instanceOf(SingleGeneratedAdapterObserver.class));
+ assertThat(actual, is(asList("onCreate", "onAny")));
+ }
+
+ interface OnPauses extends LifecycleObserver {
+ @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
+ void onPause();
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
+ void onPause(LifecycleOwner owner);
+ }
+
+ interface OnPauseResume extends LifecycleObserver {
+ @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
+ void onPause();
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
+ void onResume();
+ }
+
+ class Impl1 implements OnPauses, OnPauseResume {
+
+ List<String> mLog;
+
+ Impl1(List<String> log) {
+ mLog = log;
+ }
+
+ @Override
+ public void onPause() {
+ mLog.add("onPause_0");
+ }
+
+ @Override
+ public void onResume() {
+ mLog.add("onResume");
+ }
+
+ @Override
+ public void onPause(LifecycleOwner owner) {
+ mLog.add("onPause_1");
+ }
+ }
+
+ @Test
+ public void testClashingInterfaces() {
+ List<String> actual = new ArrayList<>();
+ GenericLifecycleObserver callback = Lifecycling.getCallback(new Impl1(actual));
+ callback.onStateChanged(mOwner, Lifecycle.Event.ON_PAUSE);
+ assertThat(callback, instanceOf(CompositeGeneratedAdaptersObserver.class));
+ assertThat(actual, is(asList("onPause_0", "onPause_1")));
+ actual.clear();
+ callback.onStateChanged(mOwner, Lifecycle.Event.ON_RESUME);
+ assertThat(actual, is(singletonList("onResume")));
+ }
+
+ class Base implements LifecycleObserver {
+
+ List<String> mLog;
+
+ Base(List<String> log) {
+ mLog = log;
+ }
+
+ @OnLifecycleEvent(ON_ANY)
+ void onAny() {
+ mLog.add("onAny_0");
+ }
+
+ @OnLifecycleEvent(ON_ANY)
+ void onAny(LifecycleOwner owner) {
+ mLog.add("onAny_1");
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
+ void onResume() {
+ mLog.add("onResume");
+ }
+ }
+
+ interface OnAny extends LifecycleObserver {
+ @OnLifecycleEvent(ON_ANY)
+ void onAny();
+
+ @OnLifecycleEvent(ON_ANY)
+ void onAny(LifecycleOwner owner, Lifecycle.Event event);
+ }
+
+ class Derived extends Base implements OnAny {
+ Derived(List<String> log) {
+ super(log);
+ }
+
+ @Override
+ public void onAny() {
+ super.onAny();
+ }
+
+ @Override
+ public void onAny(LifecycleOwner owner, Lifecycle.Event event) {
+ mLog.add("onAny_2");
+ assertThat(event, is(ON_RESUME));
+ }
+ }
+
+ @Test
+ public void testClashingClassAndInterface() {
+ List<String> actual = new ArrayList<>();
+ GenericLifecycleObserver callback = Lifecycling.getCallback(new Derived(actual));
+ callback.onStateChanged(mOwner, Lifecycle.Event.ON_RESUME);
+ assertThat(callback, instanceOf(CompositeGeneratedAdaptersObserver.class));
+ assertThat(actual, is(asList("onResume", "onAny_0", "onAny_1", "onAny_2")));
+ }
+
+}
diff --git a/android/arch/lifecycle/Lifecycle.java b/android/arch/lifecycle/Lifecycle.java
index fcbd50a..02db5ff 100644
--- a/android/arch/lifecycle/Lifecycle.java
+++ b/android/arch/lifecycle/Lifecycle.java
@@ -34,7 +34,21 @@
* before {@link android.app.Activity#onStop onStop} is called.
* This gives you certain guarantees on which state the owner is in.
* <p>
- * Lifecycle events are observed using annotations.
+ * If you use <b>Java 8 Language</b>, then observe events with {@link DefaultLifecycleObserver}.
+ * To include it you should add {@code "android.arch.lifecycle:common-java8:<version>"} to your
+ * build.gradle file.
+ * <pre>
+ * class TestObserver implements DefaultLifecycleObserver {
+ * {@literal @}Override
+ * public void onCreate(LifecycleOwner owner) {
+ * // your code
+ * }
+ * }
+ * </pre>
+ * If you use <b>Java 7 Language</b>, Lifecycle events are observed using annotations.
+ * Once Java 8 Language becomes mainstream on Android, annotations will be deprecated, so between
+ * {@link DefaultLifecycleObserver} and annotations,
+ * you must always prefer {@code DefaultLifecycleObserver}.
* <pre>
* class TestObserver implements LifecycleObserver {
* {@literal @}OnLifecycleEvent(ON_STOP)
@@ -42,16 +56,6 @@
* }
* </pre>
* <p>
- * Multiple methods can observe the same event.
- * <pre>
- * class TestObserver implements LifecycleObserver {
- * {@literal @}OnLifecycleEvent(ON_STOP)
- * void onStoppedFirst() {}
- * {@literal @}OnLifecycleEvent(ON_STOP)
- * void onStoppedSecond() {}
- * }
- * </pre>
- * <p>
* Observer methods can receive zero or one argument.
* If used, the first argument must be of type {@link LifecycleOwner}.
* Methods annotated with {@link Event#ON_ANY} can receive the second argument, which must be
diff --git a/android/arch/lifecycle/Lifecycling.java b/android/arch/lifecycle/Lifecycling.java
index 3a5c0b9..7d6b37f 100644
--- a/android/arch/lifecycle/Lifecycling.java
+++ b/android/arch/lifecycle/Lifecycling.java
@@ -22,52 +22,61 @@
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
/**
* Internal class to handle lifecycle conversion etc.
+ *
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class Lifecycling {
- private static Constructor<? extends GenericLifecycleObserver> sREFLECTIVE;
+public class Lifecycling {
- static {
- try {
- sREFLECTIVE = ReflectiveGenericLifecycleObserver.class
- .getDeclaredConstructor(Object.class);
- } catch (NoSuchMethodException ignored) {
+ private static final int REFLECTIVE_CALLBACK = 1;
+ private static final int GENERATED_CALLBACK = 2;
- }
- }
-
- private static Map<Class, Constructor<? extends GenericLifecycleObserver>> sCallbackCache =
+ private static Map<Class, Integer> sCallbackCache = new HashMap<>();
+ private static Map<Class, List<Constructor<? extends GeneratedAdapter>>> sClassToAdapters =
new HashMap<>();
@NonNull
static GenericLifecycleObserver getCallback(Object object) {
+ if (object instanceof FullLifecycleObserver) {
+ return new FullLifecycleObserverAdapter((FullLifecycleObserver) object);
+ }
+
if (object instanceof GenericLifecycleObserver) {
return (GenericLifecycleObserver) object;
}
+
+ final Class<?> klass = object.getClass();
+ int type = getObserverConstructorType(klass);
+ if (type == GENERATED_CALLBACK) {
+ List<Constructor<? extends GeneratedAdapter>> constructors =
+ sClassToAdapters.get(klass);
+ if (constructors.size() == 1) {
+ GeneratedAdapter generatedAdapter = createGeneratedAdapter(
+ constructors.get(0), object);
+ return new SingleGeneratedAdapterObserver(generatedAdapter);
+ }
+ GeneratedAdapter[] adapters = new GeneratedAdapter[constructors.size()];
+ for (int i = 0; i < constructors.size(); i++) {
+ adapters[i] = createGeneratedAdapter(constructors.get(i), object);
+ }
+ return new CompositeGeneratedAdaptersObserver(adapters);
+ }
+ return new ReflectiveGenericLifecycleObserver(object);
+ }
+
+ private static GeneratedAdapter createGeneratedAdapter(
+ Constructor<? extends GeneratedAdapter> constructor, Object object) {
//noinspection TryWithIdenticalCatches
try {
- final Class<?> klass = object.getClass();
- Constructor<? extends GenericLifecycleObserver> cachedConstructor = sCallbackCache.get(
- klass);
- if (cachedConstructor != null) {
- return cachedConstructor.newInstance(object);
- }
- cachedConstructor = getGeneratedAdapterConstructor(klass);
- if (cachedConstructor != null) {
- if (!cachedConstructor.isAccessible()) {
- cachedConstructor.setAccessible(true);
- }
- } else {
- cachedConstructor = sREFLECTIVE;
- }
- sCallbackCache.put(klass, cachedConstructor);
- return cachedConstructor.newInstance(object);
+ return constructor.newInstance(object);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
@@ -78,37 +87,95 @@
}
@Nullable
- private static Constructor<? extends GenericLifecycleObserver> getGeneratedAdapterConstructor(
- Class<?> klass) {
- Package aPackage = klass.getPackage();
- final String fullPackage = aPackage != null ? aPackage.getName() : "";
-
- String name = klass.getCanonicalName();
- // anonymous class bug:35073837
- if (name == null) {
- return null;
- }
- final String adapterName = getAdapterName(fullPackage.isEmpty() ? name :
- name.substring(fullPackage.length() + 1));
+ private static Constructor<? extends GeneratedAdapter> generatedConstructor(Class<?> klass) {
try {
- @SuppressWarnings("unchecked")
- final Class<? extends GenericLifecycleObserver> aClass =
- (Class<? extends GenericLifecycleObserver>) Class.forName(
+ Package aPackage = klass.getPackage();
+ String name = klass.getCanonicalName();
+ final String fullPackage = aPackage != null ? aPackage.getName() : "";
+ final String adapterName = getAdapterName(fullPackage.isEmpty() ? name :
+ name.substring(fullPackage.length() + 1));
+
+ @SuppressWarnings("unchecked") final Class<? extends GeneratedAdapter> aClass =
+ (Class<? extends GeneratedAdapter>) Class.forName(
fullPackage.isEmpty() ? adapterName : fullPackage + "." + adapterName);
- return aClass.getDeclaredConstructor(klass);
- } catch (ClassNotFoundException e) {
- final Class<?> superclass = klass.getSuperclass();
- if (superclass != null) {
- return getGeneratedAdapterConstructor(superclass);
+ Constructor<? extends GeneratedAdapter> constructor =
+ aClass.getDeclaredConstructor(klass);
+ if (!constructor.isAccessible()) {
+ constructor.setAccessible(true);
}
+ return constructor;
+ } catch (ClassNotFoundException e) {
+ return null;
} catch (NoSuchMethodException e) {
// this should not happen
throw new RuntimeException(e);
}
- return null;
}
- static String getAdapterName(String className) {
+ private static int getObserverConstructorType(Class<?> klass) {
+ if (sCallbackCache.containsKey(klass)) {
+ return sCallbackCache.get(klass);
+ }
+ int type = resolveObserverCallbackType(klass);
+ sCallbackCache.put(klass, type);
+ return type;
+ }
+
+ private static int resolveObserverCallbackType(Class<?> klass) {
+ // anonymous class bug:35073837
+ if (klass.getCanonicalName() == null) {
+ return REFLECTIVE_CALLBACK;
+ }
+
+ Constructor<? extends GeneratedAdapter> constructor = generatedConstructor(klass);
+ if (constructor != null) {
+ sClassToAdapters.put(klass, Collections
+ .<Constructor<? extends GeneratedAdapter>>singletonList(constructor));
+ return GENERATED_CALLBACK;
+ }
+
+ boolean hasLifecycleMethods = ClassesInfoCache.sInstance.hasLifecycleMethods(klass);
+ if (hasLifecycleMethods) {
+ return REFLECTIVE_CALLBACK;
+ }
+
+ Class<?> superclass = klass.getSuperclass();
+ List<Constructor<? extends GeneratedAdapter>> adapterConstructors = null;
+ if (isLifecycleParent(superclass)) {
+ if (getObserverConstructorType(superclass) == REFLECTIVE_CALLBACK) {
+ return REFLECTIVE_CALLBACK;
+ }
+ adapterConstructors = new ArrayList<>(sClassToAdapters.get(superclass));
+ }
+
+ for (Class<?> intrface : klass.getInterfaces()) {
+ if (!isLifecycleParent(intrface)) {
+ continue;
+ }
+ if (getObserverConstructorType(intrface) == REFLECTIVE_CALLBACK) {
+ return REFLECTIVE_CALLBACK;
+ }
+ if (adapterConstructors == null) {
+ adapterConstructors = new ArrayList<>();
+ }
+ adapterConstructors.addAll(sClassToAdapters.get(intrface));
+ }
+ if (adapterConstructors != null) {
+ sClassToAdapters.put(klass, adapterConstructors);
+ return GENERATED_CALLBACK;
+ }
+
+ return REFLECTIVE_CALLBACK;
+ }
+
+ private static boolean isLifecycleParent(Class<?> klass) {
+ return klass != null && LifecycleObserver.class.isAssignableFrom(klass);
+ }
+
+ /**
+ * Create a name for an adapter class.
+ */
+ public static String getAdapterName(String className) {
return className.replace(".", "_") + "_LifecycleAdapter";
}
}
diff --git a/android/arch/lifecycle/LifecyclingTest.java b/android/arch/lifecycle/LifecyclingTest.java
new file mode 100644
index 0000000..70ce84c
--- /dev/null
+++ b/android/arch/lifecycle/LifecyclingTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.arch.lifecycle.observers.DerivedSequence1;
+import android.arch.lifecycle.observers.DerivedSequence2;
+import android.arch.lifecycle.observers.DerivedWithNewMethods;
+import android.arch.lifecycle.observers.DerivedWithNoNewMethods;
+import android.arch.lifecycle.observers.DerivedWithOverridenMethodsWithLfAnnotation;
+import android.arch.lifecycle.observers.InterfaceImpl1;
+import android.arch.lifecycle.observers.InterfaceImpl2;
+import android.arch.lifecycle.observers.InterfaceImpl3;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class LifecyclingTest {
+
+ @Test
+ public void testDerivedWithNewLfMethodsNoGeneratedAdapter() {
+ GenericLifecycleObserver callback = Lifecycling.getCallback(new DerivedWithNewMethods());
+ assertThat(callback, instanceOf(ReflectiveGenericLifecycleObserver.class));
+ }
+
+ @Test
+ public void testDerivedWithNoNewLfMethodsNoGeneratedAdapter() {
+ GenericLifecycleObserver callback = Lifecycling.getCallback(new DerivedWithNoNewMethods());
+ assertThat(callback, instanceOf(SingleGeneratedAdapterObserver.class));
+ }
+
+ @Test
+ public void testDerivedWithOverridenMethodsNoGeneratedAdapter() {
+ GenericLifecycleObserver callback = Lifecycling.getCallback(
+ new DerivedWithOverridenMethodsWithLfAnnotation());
+ // that is not effective but...
+ assertThat(callback, instanceOf(ReflectiveGenericLifecycleObserver.class));
+ }
+
+ @Test
+ public void testInterfaceImpl1NoGeneratedAdapter() {
+ GenericLifecycleObserver callback = Lifecycling.getCallback(new InterfaceImpl1());
+ assertThat(callback, instanceOf(SingleGeneratedAdapterObserver.class));
+ }
+
+ @Test
+ public void testInterfaceImpl2NoGeneratedAdapter() {
+ GenericLifecycleObserver callback = Lifecycling.getCallback(new InterfaceImpl2());
+ assertThat(callback, instanceOf(CompositeGeneratedAdaptersObserver.class));
+ }
+
+ @Test
+ public void testInterfaceImpl3NoGeneratedAdapter() {
+ GenericLifecycleObserver callback = Lifecycling.getCallback(new InterfaceImpl3());
+ assertThat(callback, instanceOf(CompositeGeneratedAdaptersObserver.class));
+ }
+
+ @Test
+ public void testDerivedSequence() {
+ GenericLifecycleObserver callback2 = Lifecycling.getCallback(new DerivedSequence2());
+ assertThat(callback2, instanceOf(ReflectiveGenericLifecycleObserver.class));
+ GenericLifecycleObserver callback1 = Lifecycling.getCallback(new DerivedSequence1());
+ assertThat(callback1, instanceOf(SingleGeneratedAdapterObserver.class));
+ }
+}
diff --git a/android/arch/lifecycle/LiveData.java b/android/arch/lifecycle/LiveData.java
index 99d859c..3aea6ac 100644
--- a/android/arch/lifecycle/LiveData.java
+++ b/android/arch/lifecycle/LiveData.java
@@ -1,411 +1,4 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
+//LiveData interface for tests
package android.arch.lifecycle;
-
-import static android.arch.lifecycle.Lifecycle.State.DESTROYED;
-import static android.arch.lifecycle.Lifecycle.State.STARTED;
-
-import android.arch.core.executor.AppToolkitTaskExecutor;
-import android.arch.core.internal.SafeIterableMap;
-import android.arch.lifecycle.Lifecycle.State;
-import android.support.annotation.MainThread;
-import android.support.annotation.Nullable;
-
-import java.util.Iterator;
-import java.util.Map;
-
-/**
- * LiveData is a data holder class that can be observed within a given lifecycle.
- * This means that an {@link Observer} can be added in a pair with a {@link LifecycleOwner}, and
- * this observer will be notified about modifications of the wrapped data only if the paired
- * LifecycleOwner is in active state. LifecycleOwner is considered as active, if its state is
- * {@link Lifecycle.State#STARTED} or {@link Lifecycle.State#RESUMED}. An observer added via
- * {@link #observeForever(Observer)} is considered as always active and thus will be always notified
- * about modifications. For those observers, you should manually call
- * {@link #removeObserver(Observer)}.
- *
- * <p> An observer added with a Lifecycle will be automatically removed if the corresponding
- * Lifecycle moves to {@link Lifecycle.State#DESTROYED} state. This is especially useful for
- * activities and fragments where they can safely observe LiveData and not worry about leaks:
- * they will be instantly unsubscribed when they are destroyed.
- *
- * <p>
- * In addition, LiveData has {@link LiveData#onActive()} and {@link LiveData#onInactive()} methods
- * to get notified when number of active {@link Observer}s change between 0 and 1.
- * This allows LiveData to release any heavy resources when it does not have any Observers that
- * are actively observing.
- * <p>
- * This class is designed to hold individual data fields of {@link ViewModel},
- * but can also be used for sharing data between different modules in your application
- * in a decoupled fashion.
- *
- * @param <T> The type of data hold by this instance
- * @see ViewModel
- */
-@SuppressWarnings({"WeakerAccess", "unused"})
-// TODO: Thread checks are too strict right now, we may consider automatically moving them to main
-// thread.
-public abstract class LiveData<T> {
- private final Object mDataLock = new Object();
- static final int START_VERSION = -1;
- private static final Object NOT_SET = new Object();
-
- private static final LifecycleOwner ALWAYS_ON = new LifecycleOwner() {
-
- private LifecycleRegistry mRegistry = init();
-
- private LifecycleRegistry init() {
- LifecycleRegistry registry = new LifecycleRegistry(this);
- registry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
- registry.handleLifecycleEvent(Lifecycle.Event.ON_START);
- registry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
- return registry;
- }
-
- @Override
- public Lifecycle getLifecycle() {
- return mRegistry;
- }
- };
-
- private SafeIterableMap<Observer<T>, LifecycleBoundObserver> mObservers =
- new SafeIterableMap<>();
-
- // how many observers are in active state
- private int mActiveCount = 0;
- private volatile Object mData = NOT_SET;
- // when setData is called, we set the pending data and actual data swap happens on the main
- // thread
- private volatile Object mPendingData = NOT_SET;
- private int mVersion = START_VERSION;
-
- private boolean mDispatchingValue;
- @SuppressWarnings("FieldCanBeLocal")
- private boolean mDispatchInvalidated;
- private final Runnable mPostValueRunnable = new Runnable() {
- @Override
- public void run() {
- Object newValue;
- synchronized (mDataLock) {
- newValue = mPendingData;
- mPendingData = NOT_SET;
- }
- //noinspection unchecked
- setValue((T) newValue);
- }
- };
-
- private void considerNotify(LifecycleBoundObserver observer) {
- if (!observer.active) {
- return;
- }
- // Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
- //
- // we still first check observer.active to keep it as the entrance for events. So even if
- // the observer moved to an active state, if we've not received that event, we better not
- // notify for a more predictable notification order.
- if (!isActiveState(observer.owner.getLifecycle().getCurrentState())) {
- return;
- }
- if (observer.lastVersion >= mVersion) {
- return;
- }
- observer.lastVersion = mVersion;
- //noinspection unchecked
- observer.observer.onChanged((T) mData);
- }
-
- private void dispatchingValue(@Nullable LifecycleBoundObserver initiator) {
- if (mDispatchingValue) {
- mDispatchInvalidated = true;
- return;
- }
- mDispatchingValue = true;
- do {
- mDispatchInvalidated = false;
- if (initiator != null) {
- considerNotify(initiator);
- initiator = null;
- } else {
- for (Iterator<Map.Entry<Observer<T>, LifecycleBoundObserver>> iterator =
- mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
- considerNotify(iterator.next().getValue());
- if (mDispatchInvalidated) {
- break;
- }
- }
- }
- } while (mDispatchInvalidated);
- mDispatchingValue = false;
- }
-
- /**
- * Adds the given observer to the observers list within the lifespan of the given
- * owner. The events are dispatched on the main thread. If LiveData already has data
- * set, it will be delivered to the observer.
- * <p>
- * The observer will only receive events if the owner is in {@link Lifecycle.State#STARTED}
- * or {@link Lifecycle.State#RESUMED} state (active).
- * <p>
- * If the owner moves to the {@link Lifecycle.State#DESTROYED} state, the observer will
- * automatically be removed.
- * <p>
- * When data changes while the {@code owner} is not active, it will not receive any updates.
- * If it becomes active again, it will receive the last available data automatically.
- * <p>
- * LiveData keeps a strong reference to the observer and the owner as long as the
- * given LifecycleOwner is not destroyed. When it is destroyed, LiveData removes references to
- * the observer & the owner.
- * <p>
- * If the given owner is already in {@link Lifecycle.State#DESTROYED} state, LiveData
- * ignores the call.
- * <p>
- * If the given owner, observer tuple is already in the list, the call is ignored.
- * If the observer is already in the list with another owner, LiveData throws an
- * {@link IllegalArgumentException}.
- *
- * @param owner The LifecycleOwner which controls the observer
- * @param observer The observer that will receive the events
- */
- @MainThread
- public void observe(LifecycleOwner owner, Observer<T> observer) {
- if (owner.getLifecycle().getCurrentState() == DESTROYED) {
- // ignore
- return;
- }
- LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
- LifecycleBoundObserver existing = mObservers.putIfAbsent(observer, wrapper);
- if (existing != null && existing.owner != wrapper.owner) {
- throw new IllegalArgumentException("Cannot add the same observer"
- + " with different lifecycles");
- }
- if (existing != null) {
- return;
- }
- owner.getLifecycle().addObserver(wrapper);
- wrapper.activeStateChanged(isActiveState(owner.getLifecycle().getCurrentState()));
- }
-
- /**
- * Adds the given observer to the observers list. This call is similar to
- * {@link LiveData#observe(LifecycleOwner, Observer)} with a LifecycleOwner, which
- * is always active. This means that the given observer will receive all events and will never
- * be automatically removed. You should manually call {@link #removeObserver(Observer)} to stop
- * observing this LiveData.
- * While LiveData has one of such observers, it will be considered
- * as active.
- * <p>
- * If the observer was already added with an owner to this LiveData, LiveData throws an
- * {@link IllegalArgumentException}.
- *
- * @param observer The observer that will receive the events
- */
- @MainThread
- public void observeForever(Observer<T> observer) {
- observe(ALWAYS_ON, observer);
- }
-
- /**
- * Removes the given observer from the observers list.
- *
- * @param observer The Observer to receive events.
- */
- @MainThread
- public void removeObserver(final Observer<T> observer) {
- assertMainThread("removeObserver");
- LifecycleBoundObserver removed = mObservers.remove(observer);
- if (removed == null) {
- return;
- }
- removed.owner.getLifecycle().removeObserver(removed);
- removed.activeStateChanged(false);
- }
-
- /**
- * Removes all observers that are tied to the given {@link LifecycleOwner}.
- *
- * @param owner The {@code LifecycleOwner} scope for the observers to be removed.
- */
- @MainThread
- public void removeObservers(final LifecycleOwner owner) {
- assertMainThread("removeObservers");
- for (Map.Entry<Observer<T>, LifecycleBoundObserver> entry : mObservers) {
- if (entry.getValue().owner == owner) {
- removeObserver(entry.getKey());
- }
- }
- }
-
- /**
- * Posts a task to a main thread to set the given value. So if you have a following code
- * executed in the main thread:
- * <pre class="prettyprint">
- * liveData.postValue("a");
- * liveData.setValue("b");
- * </pre>
- * The value "b" would be set at first and later the main thread would override it with
- * the value "a".
- * <p>
- * If you called this method multiple times before a main thread executed a posted task, only
- * the last value would be dispatched.
- *
- * @param value The new value
- */
- protected void postValue(T value) {
- boolean postTask;
- synchronized (mDataLock) {
- postTask = mPendingData == NOT_SET;
- mPendingData = value;
- }
- if (!postTask) {
- return;
- }
- AppToolkitTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
- }
-
- /**
- * Sets the value. If there are active observers, the value will be dispatched to them.
- * <p>
- * This method must be called from the main thread. If you need set a value from a background
- * thread, you can use {@link #postValue(Object)}
- *
- * @param value The new value
- */
- @MainThread
- protected void setValue(T value) {
- assertMainThread("setValue");
- mVersion++;
- mData = value;
- dispatchingValue(null);
- }
-
- /**
- * Returns the current value.
- * Note that calling this method on a background thread does not guarantee that the latest
- * value set will be received.
- *
- * @return the current value
- */
- @Nullable
- public T getValue() {
- Object data = mData;
- if (data != NOT_SET) {
- //noinspection unchecked
- return (T) data;
- }
- return null;
- }
-
- int getVersion() {
- return mVersion;
- }
-
- /**
- * Called when the number of active observers change to 1 from 0.
- * <p>
- * This callback can be used to know that this LiveData is being used thus should be kept
- * up to date.
- */
- protected void onActive() {
-
- }
-
- /**
- * Called when the number of active observers change from 1 to 0.
- * <p>
- * This does not mean that there are no observers left, there may still be observers but their
- * lifecycle states aren't {@link Lifecycle.State#STARTED} or {@link Lifecycle.State#RESUMED}
- * (like an Activity in the back stack).
- * <p>
- * You can check if there are observers via {@link #hasObservers()}.
- */
- protected void onInactive() {
-
- }
-
- /**
- * Returns true if this LiveData has observers.
- *
- * @return true if this LiveData has observers
- */
- public boolean hasObservers() {
- return mObservers.size() > 0;
- }
-
- /**
- * Returns true if this LiveData has active observers.
- *
- * @return true if this LiveData has active observers
- */
- public boolean hasActiveObservers() {
- return mActiveCount > 0;
- }
-
- class LifecycleBoundObserver implements LifecycleObserver {
- public final LifecycleOwner owner;
- public final Observer<T> observer;
- public boolean active;
- public int lastVersion = START_VERSION;
-
- LifecycleBoundObserver(LifecycleOwner owner, Observer<T> observer) {
- this.owner = owner;
- this.observer = observer;
- }
-
- @SuppressWarnings("unused")
- @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
- void onStateChange() {
- if (owner.getLifecycle().getCurrentState() == DESTROYED) {
- removeObserver(observer);
- return;
- }
- // immediately set active state, so we'd never dispatch anything to inactive
- // owner
- activeStateChanged(isActiveState(owner.getLifecycle().getCurrentState()));
-
- }
-
- void activeStateChanged(boolean newActive) {
- if (newActive == active) {
- return;
- }
- active = newActive;
- boolean wasInactive = LiveData.this.mActiveCount == 0;
- LiveData.this.mActiveCount += active ? 1 : -1;
- if (wasInactive && active) {
- onActive();
- }
- if (LiveData.this.mActiveCount == 0 && !active) {
- onInactive();
- }
- if (active) {
- dispatchingValue(this);
- }
- }
- }
-
- static boolean isActiveState(State state) {
- return state.isAtLeast(STARTED);
- }
-
- private void assertMainThread(String methodName) {
- if (!AppToolkitTaskExecutor.getInstance().isMainThread()) {
- throw new IllegalStateException("Cannot invoke " + methodName + " on a background"
- + " thread");
- }
- }
+public class LiveData<T> {
}
diff --git a/android/arch/lifecycle/LiveDataReactiveStreams.java b/android/arch/lifecycle/LiveDataReactiveStreams.java
index 0be0149..2b25bc9 100644
--- a/android/arch/lifecycle/LiveDataReactiveStreams.java
+++ b/android/arch/lifecycle/LiveDataReactiveStreams.java
@@ -16,7 +16,8 @@
package android.arch.lifecycle;
-import android.arch.core.executor.AppToolkitTaskExecutor;
+import android.arch.core.executor.ArchTaskExecutor;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.reactivestreams.Publisher;
@@ -85,7 +86,7 @@
if (n < 0 || mCanceled) {
return;
}
- AppToolkitTaskExecutor.getInstance().executeOnMainThread(new Runnable() {
+ ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() {
@Override
public void run() {
if (mCanceled) {
@@ -110,7 +111,7 @@
if (mCanceled) {
return;
}
- AppToolkitTaskExecutor.getInstance().executeOnMainThread(new Runnable() {
+ ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() {
@Override
public void run() {
if (mCanceled) {
@@ -133,40 +134,101 @@
/**
* Creates an Observable {@link LiveData} stream from a ReactiveStreams publisher.
+ *
+ * <p>
+ * When the LiveData becomes active, it subscribes to the emissions from the Publisher.
+ *
+ * <p>
+ * When the LiveData becomes inactive, the subscription is cleared.
+ * LiveData holds the last value emitted by the Publisher when the LiveData was active.
+ * <p>
+ * Therefore, in the case of a hot RxJava Observable, when a new LiveData {@link Observer} is
+ * added, it will automatically notify with the last value held in LiveData,
+ * which might not be the last value emitted by the Publisher.
+ *
+ * @param <T> The type of data hold by this instance.
*/
public static <T> LiveData<T> fromPublisher(final Publisher<T> publisher) {
- MutableLiveData<T> liveData = new MutableLiveData<>();
- // Since we don't have a way to directly observe cancels, weakly hold the live data.
- final WeakReference<MutableLiveData<T>> liveDataRef = new WeakReference<>(liveData);
-
- publisher.subscribe(new Subscriber<T>() {
- @Override
- public void onSubscribe(Subscription s) {
- // Don't worry about backpressure. If the stream is too noisy then backpressure can
- // be handled upstream.
- s.request(Long.MAX_VALUE);
- }
-
- @Override
- public void onNext(final T t) {
- final LiveData<T> liveData = liveDataRef.get();
- if (liveData != null) {
- liveData.postValue(t);
- }
- }
-
- @Override
- public void onError(Throwable t) {
- // Errors should be handled upstream, so propagate as a crash.
- throw new RuntimeException(t);
- }
-
- @Override
- public void onComplete() {
- }
- });
-
- return liveData;
+ return new PublisherLiveData<>(publisher);
}
+ /**
+ * Defines a {@link LiveData} object that wraps a {@link Publisher}.
+ *
+ * <p>
+ * When the LiveData becomes active, it subscribes to the emissions from the Publisher.
+ *
+ * <p>
+ * When the LiveData becomes inactive, the subscription is cleared.
+ * LiveData holds the last value emitted by the Publisher when the LiveData was active.
+ * <p>
+ * Therefore, in the case of a hot RxJava Observable, when a new LiveData {@link Observer} is
+ * added, it will automatically notify with the last value held in LiveData,
+ * which might not be the last value emitted by the Publisher.
+ *
+ * @param <T> The type of data hold by this instance.
+ */
+ private static class PublisherLiveData<T> extends LiveData<T> {
+ private WeakReference<Subscription> mSubscriptionRef;
+ private final Publisher mPublisher;
+ private final Object mLock = new Object();
+
+ PublisherLiveData(@NonNull final Publisher publisher) {
+ mPublisher = publisher;
+ }
+
+ @Override
+ protected void onActive() {
+ super.onActive();
+
+ mPublisher.subscribe(new Subscriber<T>() {
+ @Override
+ public void onSubscribe(Subscription s) {
+ // Don't worry about backpressure. If the stream is too noisy then
+ // backpressure can be handled upstream.
+ synchronized (mLock) {
+ s.request(Long.MAX_VALUE);
+ mSubscriptionRef = new WeakReference<>(s);
+ }
+ }
+
+ @Override
+ public void onNext(final T t) {
+ postValue(t);
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ synchronized (mLock) {
+ mSubscriptionRef = null;
+ }
+ // Errors should be handled upstream, so propagate as a crash.
+ throw new RuntimeException(t);
+ }
+
+ @Override
+ public void onComplete() {
+ synchronized (mLock) {
+ mSubscriptionRef = null;
+ }
+ }
+ });
+
+ }
+
+ @Override
+ protected void onInactive() {
+ super.onInactive();
+ synchronized (mLock) {
+ WeakReference<Subscription> subscriptionRef = mSubscriptionRef;
+ if (subscriptionRef != null) {
+ Subscription subscription = subscriptionRef.get();
+ if (subscription != null) {
+ subscription.cancel();
+ }
+ mSubscriptionRef = null;
+ }
+ }
+ }
+ }
}
diff --git a/android/arch/lifecycle/LiveDataReactiveStreamsTest.java b/android/arch/lifecycle/LiveDataReactiveStreamsTest.java
index 87fba27..7278847 100644
--- a/android/arch/lifecycle/LiveDataReactiveStreamsTest.java
+++ b/android/arch/lifecycle/LiveDataReactiveStreamsTest.java
@@ -16,12 +16,10 @@
package android.arch.lifecycle;
-import static android.arch.lifecycle.Lifecycle.State.RESUMED;
-
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
-import android.arch.core.executor.AppToolkitTaskExecutor;
+import android.arch.core.executor.ArchTaskExecutor;
import android.arch.core.executor.TaskExecutor;
import android.support.annotation.Nullable;
import android.support.test.filters.SmallTest;
@@ -47,28 +45,7 @@
@SmallTest
public class LiveDataReactiveStreamsTest {
- private static final Lifecycle sLifecycle = new Lifecycle() {
- @Override
- public void addObserver(LifecycleObserver observer) {
- }
-
- @Override
- public void removeObserver(LifecycleObserver observer) {
- }
-
- @Override
- public State getCurrentState() {
- return RESUMED;
- }
- };
- private static final LifecycleOwner S_LIFECYCLE_OWNER = new LifecycleOwner() {
-
- @Override
- public Lifecycle getLifecycle() {
- return sLifecycle;
- }
-
- };
+ private LifecycleOwner mLifecycleOwner;
private final List<String> mLiveDataOutput = new ArrayList<>();
private final Observer<String> mObserver = new Observer<String>() {
@@ -85,8 +62,19 @@
@Before
public void init() {
+ mLifecycleOwner = new LifecycleOwner() {
+ LifecycleRegistry mRegistry = new LifecycleRegistry(this);
+ {
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
+ }
+
+ @Override
+ public Lifecycle getLifecycle() {
+ return mRegistry;
+ }
+ };
mTestThread = Thread.currentThread();
- AppToolkitTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
+ ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
@Override
public void executeOnDiskIO(Runnable runnable) {
@@ -109,7 +97,7 @@
@After
public void removeExecutorDelegate() {
- AppToolkitTaskExecutor.getInstance().setDelegate(null);
+ ArchTaskExecutor.getInstance().setDelegate(null);
}
@Test
@@ -117,7 +105,7 @@
PublishProcessor<String> processor = PublishProcessor.create();
LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor);
- liveData.observe(S_LIFECYCLE_OWNER, mObserver);
+ liveData.observe(mLifecycleOwner, mObserver);
processor.onNext("foo");
processor.onNext("bar");
@@ -132,13 +120,13 @@
PublishProcessor<String> processor = PublishProcessor.create();
LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor);
- liveData.observe(S_LIFECYCLE_OWNER, mObserver);
+ liveData.observe(mLifecycleOwner, mObserver);
processor.onNext("foo");
processor.onNext("bar");
// The second mObserver should only get the newest value and any later values.
- liveData.observe(S_LIFECYCLE_OWNER, new Observer<String>() {
+ liveData.observe(mLifecycleOwner, new Observer<String>() {
@Override
public void onChanged(@Nullable String s) {
output2.add(s);
@@ -152,12 +140,44 @@
}
@Test
+ public void convertsFromPublisherAfterInactive() {
+ PublishProcessor<String> processor = PublishProcessor.create();
+ LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor);
+
+ liveData.observe(mLifecycleOwner, mObserver);
+ processor.onNext("foo");
+ liveData.removeObserver(mObserver);
+ processor.onNext("bar");
+
+ liveData.observe(mLifecycleOwner, mObserver);
+ processor.onNext("baz");
+
+ assertThat(mLiveDataOutput, is(Arrays.asList("foo", "foo", "baz")));
+ }
+
+ @Test
+ public void convertsFromPublisherManagesSubcriptions() {
+ PublishProcessor<String> processor = PublishProcessor.create();
+ LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor);
+
+ assertThat(processor.hasSubscribers(), is(false));
+ liveData.observe(mLifecycleOwner, mObserver);
+
+ // once the live data is active, there's a subscriber
+ assertThat(processor.hasSubscribers(), is(true));
+
+ liveData.removeObserver(mObserver);
+ // once the live data is inactive, the subscriber is removed
+ assertThat(processor.hasSubscribers(), is(false));
+ }
+
+ @Test
public void convertsFromAsyncPublisher() {
Flowable<String> input = Flowable.just("foo")
.concatWith(Flowable.just("bar", "baz").observeOn(sBackgroundScheduler));
LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(input);
- liveData.observe(S_LIFECYCLE_OWNER, mObserver);
+ liveData.observe(mLifecycleOwner, mObserver);
assertThat(mLiveDataOutput, is(Collections.singletonList("foo")));
sBackgroundScheduler.triggerActions();
@@ -170,7 +190,7 @@
liveData.setValue("foo");
assertThat(liveData.getValue(), is("foo"));
- Flowable.fromPublisher(LiveDataReactiveStreams.toPublisher(S_LIFECYCLE_OWNER, liveData))
+ Flowable.fromPublisher(LiveDataReactiveStreams.toPublisher(mLifecycleOwner, liveData))
.subscribe(mOutputProcessor);
liveData.setValue("bar");
@@ -188,7 +208,7 @@
assertThat(liveData.getValue(), is("foo"));
Disposable disposable = Flowable
- .fromPublisher(LiveDataReactiveStreams.toPublisher(S_LIFECYCLE_OWNER, liveData))
+ .fromPublisher(LiveDataReactiveStreams.toPublisher(mLifecycleOwner, liveData))
.subscribe(new Consumer<String>() {
@Override
public void accept(String s) throws Exception {
@@ -216,7 +236,7 @@
final AsyncSubject<Subscription> subscriptionSubject = AsyncSubject.create();
- Flowable.fromPublisher(LiveDataReactiveStreams.toPublisher(S_LIFECYCLE_OWNER, liveData))
+ Flowable.fromPublisher(LiveDataReactiveStreams.toPublisher(mLifecycleOwner, liveData))
.subscribe(new Subscriber<String>() {
@Override
public void onSubscribe(Subscription s) {
@@ -275,7 +295,7 @@
public void convertsToPublisherWithAsyncData() {
MutableLiveData<String> liveData = new MutableLiveData<>();
- Flowable.fromPublisher(LiveDataReactiveStreams.toPublisher(S_LIFECYCLE_OWNER, liveData))
+ Flowable.fromPublisher(LiveDataReactiveStreams.toPublisher(mLifecycleOwner, liveData))
.observeOn(sBackgroundScheduler)
.subscribe(mOutputProcessor);
diff --git a/android/arch/lifecycle/LiveDataTest.java b/android/arch/lifecycle/LiveDataTest.java
index ed2a35d..9f0b425 100644
--- a/android/arch/lifecycle/LiveDataTest.java
+++ b/android/arch/lifecycle/LiveDataTest.java
@@ -36,16 +36,20 @@
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
-import android.arch.core.executor.AppToolkitTaskExecutor;
+import android.arch.core.executor.ArchTaskExecutor;
import android.arch.lifecycle.util.InstantTaskExecutor;
import android.support.annotation.Nullable;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.InOrder;
import org.mockito.Mockito;
@SuppressWarnings({"unchecked"})
+@RunWith(JUnit4.class)
public class LiveDataTest {
private PublicLiveData<String> mLiveData;
private LifecycleOwner mOwner;
@@ -66,12 +70,12 @@
@Before
public void swapExecutorDelegate() {
- AppToolkitTaskExecutor.getInstance().setDelegate(new InstantTaskExecutor());
+ ArchTaskExecutor.getInstance().setDelegate(new InstantTaskExecutor());
}
@After
public void removeExecutorDelegate() {
- AppToolkitTaskExecutor.getInstance().setDelegate(null);
+ ArchTaskExecutor.getInstance().setDelegate(null);
}
@Test
@@ -418,6 +422,40 @@
verify(mActiveObserversChanged, never()).onCall(anyBoolean());
}
+ @Test
+ public void testRemoveDuringAddition() {
+ mRegistry.handleLifecycleEvent(ON_START);
+ mLiveData.setValue("bla");
+ mLiveData.observeForever(new Observer<String>() {
+ @Override
+ public void onChanged(@Nullable String s) {
+ mLiveData.removeObserver(this);
+ }
+ });
+ assertThat(mLiveData.hasActiveObservers(), is(false));
+ InOrder inOrder = Mockito.inOrder(mActiveObserversChanged);
+ inOrder.verify(mActiveObserversChanged).onCall(true);
+ inOrder.verify(mActiveObserversChanged).onCall(false);
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ public void testRemoveDuringBringingUpToState() {
+ mLiveData.setValue("bla");
+ mLiveData.observeForever(new Observer<String>() {
+ @Override
+ public void onChanged(@Nullable String s) {
+ mLiveData.removeObserver(this);
+ }
+ });
+ mRegistry.handleLifecycleEvent(ON_RESUME);
+ assertThat(mLiveData.hasActiveObservers(), is(false));
+ InOrder inOrder = Mockito.inOrder(mActiveObserversChanged);
+ inOrder.verify(mActiveObserversChanged).onCall(true);
+ inOrder.verify(mActiveObserversChanged).onCall(false);
+ inOrder.verifyNoMoreInteractions();
+ }
+
@SuppressWarnings("WeakerAccess")
static class PublicLiveData<T> extends LiveData<T> {
// cannot spy due to internal calls
diff --git a/android/arch/lifecycle/MediatorLiveDataTest.java b/android/arch/lifecycle/MediatorLiveDataTest.java
index 3de3eee..e2eadbe 100644
--- a/android/arch/lifecycle/MediatorLiveDataTest.java
+++ b/android/arch/lifecycle/MediatorLiveDataTest.java
@@ -25,7 +25,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import android.arch.core.executor.AppToolkitTaskExecutor;
+import android.arch.core.executor.ArchTaskExecutor;
import android.arch.lifecycle.util.InstantTaskExecutor;
import android.support.annotation.Nullable;
@@ -69,7 +69,7 @@
@Before
public void swapExecutorDelegate() {
- AppToolkitTaskExecutor.getInstance().setDelegate(new InstantTaskExecutor());
+ ArchTaskExecutor.getInstance().setDelegate(new InstantTaskExecutor());
}
@Test
diff --git a/android/arch/lifecycle/MethodCallsLogger.java b/android/arch/lifecycle/MethodCallsLogger.java
new file mode 100644
index 0000000..031e43e
--- /dev/null
+++ b/android/arch/lifecycle/MethodCallsLogger.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle;
+
+import android.support.annotation.RestrictTo;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class MethodCallsLogger {
+ private Map<String, Integer> mCalledMethods = new HashMap<>();
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public boolean approveCall(String name, int type) {
+ Integer nullableMask = mCalledMethods.get(name);
+ int mask = nullableMask != null ? nullableMask : 0;
+ boolean wasCalled = (mask & type) != 0;
+ mCalledMethods.put(name, mask | type);
+ return !wasCalled;
+ }
+}
diff --git a/android/arch/lifecycle/ProcessOwnerTest.java b/android/arch/lifecycle/ProcessOwnerTest.java
index e80e11c..37bdcdb 100644
--- a/android/arch/lifecycle/ProcessOwnerTest.java
+++ b/android/arch/lifecycle/ProcessOwnerTest.java
@@ -37,6 +37,7 @@
import android.support.test.filters.SmallTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.app.FragmentActivity;
import org.junit.After;
import org.junit.Rule;
@@ -78,7 +79,7 @@
@Test
public void testNavigation() throws Throwable {
- LifecycleActivity firstActivity = setupObserverOnResume();
+ FragmentActivity firstActivity = setupObserverOnResume();
Instrumentation.ActivityMonitor monitor = new Instrumentation.ActivityMonitor(
NavigationTestActivitySecond.class.getCanonicalName(), null, false);
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
@@ -88,15 +89,15 @@
firstActivity.finish();
firstActivity.startActivity(intent);
- LifecycleActivity secondActivity = (LifecycleActivity) monitor.waitForActivity();
+ FragmentActivity secondActivity = (FragmentActivity) monitor.waitForActivity();
assertThat("Failed to navigate", secondActivity, notNullValue());
checkProcessObserverSilent(secondActivity);
}
@Test
public void testRecreation() throws Throwable {
- LifecycleActivity activity = setupObserverOnResume();
- LifecycleActivity recreated = TestUtils.recreateActivity(activity, activityTestRule);
+ FragmentActivity activity = setupObserverOnResume();
+ FragmentActivity recreated = TestUtils.recreateActivity(activity, activityTestRule);
assertThat("Failed to recreate", recreated, notNullValue());
checkProcessObserverSilent(recreated);
}
@@ -112,14 +113,15 @@
NavigationTestActivityFirst activity = activityTestRule.getActivity();
activity.startActivity(new Intent(activity, NavigationDialogActivity.class));
- LifecycleActivity dialogActivity = (LifecycleActivity) monitor.waitForActivity();
+ FragmentActivity dialogActivity = (FragmentActivity) monitor.waitForActivity();
checkProcessObserverSilent(dialogActivity);
List<Event> events = Collections.synchronizedList(new ArrayList<>());
LifecycleObserver collectingObserver = new LifecycleObserver() {
@OnLifecycleEvent(Event.ON_ANY)
- public void onStateChanged(LifecycleOwner provider, Event event) {
+ public void onStateChanged(@SuppressWarnings("unused") LifecycleOwner provider,
+ Event event) {
events.add(event);
}
};
@@ -138,8 +140,8 @@
dialogActivity.finish();
}
- private LifecycleActivity setupObserverOnResume() throws Throwable {
- LifecycleActivity firstActivity = activityTestRule.getActivity();
+ private FragmentActivity setupObserverOnResume() throws Throwable {
+ FragmentActivity firstActivity = activityTestRule.getActivity();
waitTillResumed(firstActivity, activityTestRule);
addProcessObserver(mObserver);
mObserver.mChangedState = false;
@@ -156,7 +158,7 @@
ProcessLifecycleOwner.get().getLifecycle().removeObserver(observer));
}
- private void checkProcessObserverSilent(LifecycleActivity activity) throws Throwable {
+ private void checkProcessObserverSilent(FragmentActivity activity) throws Throwable {
waitTillResumed(activity, activityTestRule);
assertThat(mObserver.mChangedState, is(false));
activityTestRule.runOnUiThread(() ->
diff --git a/android/arch/lifecycle/ReflectiveGenericLifecycleObserver.java b/android/arch/lifecycle/ReflectiveGenericLifecycleObserver.java
index 44815e6..f010ed8 100644
--- a/android/arch/lifecycle/ReflectiveGenericLifecycleObserver.java
+++ b/android/arch/lifecycle/ReflectiveGenericLifecycleObserver.java
@@ -16,204 +16,23 @@
package android.arch.lifecycle;
+import android.arch.lifecycle.ClassesInfoCache.CallbackInfo;
import android.arch.lifecycle.Lifecycle.Event;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-
/**
* An internal implementation of {@link GenericLifecycleObserver} that relies on reflection.
*/
class ReflectiveGenericLifecycleObserver implements GenericLifecycleObserver {
private final Object mWrapped;
private final CallbackInfo mInfo;
- @SuppressWarnings("WeakerAccess")
- static final Map<Class, CallbackInfo> sInfoCache = new HashMap<>();
ReflectiveGenericLifecycleObserver(Object wrapped) {
mWrapped = wrapped;
- mInfo = getInfo(mWrapped.getClass());
+ mInfo = ClassesInfoCache.sInstance.getInfo(mWrapped.getClass());
}
@Override
public void onStateChanged(LifecycleOwner source, Event event) {
- invokeCallbacks(mInfo, source, event);
+ mInfo.invokeCallbacks(source, event, mWrapped);
}
-
- private void invokeMethodsForEvent(List<MethodReference> handlers, LifecycleOwner source,
- Event event) {
- if (handlers != null) {
- for (int i = handlers.size() - 1; i >= 0; i--) {
- MethodReference reference = handlers.get(i);
- invokeCallback(reference, source, event);
- }
- }
- }
-
- @SuppressWarnings("ConstantConditions")
- private void invokeCallbacks(CallbackInfo info, LifecycleOwner source, Event event) {
- invokeMethodsForEvent(info.mEventToHandlers.get(event), source, event);
- invokeMethodsForEvent(info.mEventToHandlers.get(Event.ON_ANY), source, event);
- }
-
- private void invokeCallback(MethodReference reference, LifecycleOwner source, Event event) {
- //noinspection TryWithIdenticalCatches
- try {
- switch (reference.mCallType) {
- case CALL_TYPE_NO_ARG:
- reference.mMethod.invoke(mWrapped);
- break;
- case CALL_TYPE_PROVIDER:
- reference.mMethod.invoke(mWrapped, source);
- break;
- case CALL_TYPE_PROVIDER_WITH_EVENT:
- reference.mMethod.invoke(mWrapped, source, event);
- break;
- }
- } catch (InvocationTargetException e) {
- throw new RuntimeException("Failed to call observer method", e.getCause());
- } catch (IllegalAccessException e) {
- throw new RuntimeException(e);
- }
- }
-
- private static CallbackInfo getInfo(Class klass) {
- CallbackInfo existing = sInfoCache.get(klass);
- if (existing != null) {
- return existing;
- }
- existing = createInfo(klass);
- return existing;
- }
-
- private static void verifyAndPutHandler(Map<MethodReference, Event> handlers,
- MethodReference newHandler, Event newEvent, Class klass) {
- Event event = handlers.get(newHandler);
- if (event != null && newEvent != event) {
- Method method = newHandler.mMethod;
- throw new IllegalArgumentException(
- "Method " + method.getName() + " in " + klass.getName()
- + " already declared with different @OnLifecycleEvent value: previous"
- + " value " + event + ", new value " + newEvent);
- }
- if (event == null) {
- handlers.put(newHandler, newEvent);
- }
- }
-
- private static CallbackInfo createInfo(Class klass) {
- Class superclass = klass.getSuperclass();
- Map<MethodReference, Event> handlerToEvent = new HashMap<>();
- if (superclass != null) {
- CallbackInfo superInfo = getInfo(superclass);
- if (superInfo != null) {
- handlerToEvent.putAll(superInfo.mHandlerToEvent);
- }
- }
-
- Method[] methods = klass.getDeclaredMethods();
-
- Class[] interfaces = klass.getInterfaces();
- for (Class intrfc : interfaces) {
- for (Entry<MethodReference, Event> entry : getInfo(intrfc).mHandlerToEvent.entrySet()) {
- verifyAndPutHandler(handlerToEvent, entry.getKey(), entry.getValue(), klass);
- }
- }
-
- for (Method method : methods) {
- OnLifecycleEvent annotation = method.getAnnotation(OnLifecycleEvent.class);
- if (annotation == null) {
- continue;
- }
- Class<?>[] params = method.getParameterTypes();
- int callType = CALL_TYPE_NO_ARG;
- if (params.length > 0) {
- callType = CALL_TYPE_PROVIDER;
- if (!params[0].isAssignableFrom(LifecycleOwner.class)) {
- throw new IllegalArgumentException(
- "invalid parameter type. Must be one and instanceof LifecycleOwner");
- }
- }
- Event event = annotation.value();
-
- if (params.length > 1) {
- callType = CALL_TYPE_PROVIDER_WITH_EVENT;
- if (!params[1].isAssignableFrom(Event.class)) {
- throw new IllegalArgumentException(
- "invalid parameter type. second arg must be an event");
- }
- if (event != Event.ON_ANY) {
- throw new IllegalArgumentException(
- "Second arg is supported only for ON_ANY value");
- }
- }
- if (params.length > 2) {
- throw new IllegalArgumentException("cannot have more than 2 params");
- }
- MethodReference methodReference = new MethodReference(callType, method);
- verifyAndPutHandler(handlerToEvent, methodReference, event, klass);
- }
- CallbackInfo info = new CallbackInfo(handlerToEvent);
- sInfoCache.put(klass, info);
- return info;
- }
-
- @SuppressWarnings("WeakerAccess")
- static class CallbackInfo {
- final Map<Event, List<MethodReference>> mEventToHandlers;
- final Map<MethodReference, Event> mHandlerToEvent;
-
- CallbackInfo(Map<MethodReference, Event> handlerToEvent) {
- mHandlerToEvent = handlerToEvent;
- mEventToHandlers = new HashMap<>();
- for (Entry<MethodReference, Event> entry : handlerToEvent.entrySet()) {
- Event event = entry.getValue();
- List<MethodReference> methodReferences = mEventToHandlers.get(event);
- if (methodReferences == null) {
- methodReferences = new ArrayList<>();
- mEventToHandlers.put(event, methodReferences);
- }
- methodReferences.add(entry.getKey());
- }
- }
- }
-
- @SuppressWarnings("WeakerAccess")
- static class MethodReference {
- final int mCallType;
- final Method mMethod;
-
- MethodReference(int callType, Method method) {
- mCallType = callType;
- mMethod = method;
- mMethod.setAccessible(true);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
-
- MethodReference that = (MethodReference) o;
- return mCallType == that.mCallType && mMethod.getName().equals(that.mMethod.getName());
- }
-
- @Override
- public int hashCode() {
- return 31 * mCallType + mMethod.getName().hashCode();
- }
- }
-
- private static final int CALL_TYPE_NO_ARG = 0;
- private static final int CALL_TYPE_PROVIDER = 1;
- private static final int CALL_TYPE_PROVIDER_WITH_EVENT = 2;
}
diff --git a/android/arch/lifecycle/SingleGeneratedAdapterObserver.java b/android/arch/lifecycle/SingleGeneratedAdapterObserver.java
new file mode 100644
index 0000000..d176a3a
--- /dev/null
+++ b/android/arch/lifecycle/SingleGeneratedAdapterObserver.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle;
+
+import android.support.annotation.RestrictTo;
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SingleGeneratedAdapterObserver implements GenericLifecycleObserver {
+
+ private final GeneratedAdapter mGeneratedAdapter;
+
+ SingleGeneratedAdapterObserver(GeneratedAdapter generatedAdapter) {
+ mGeneratedAdapter = generatedAdapter;
+ }
+
+ @Override
+ public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
+ mGeneratedAdapter.callMethods(source, event, false, null);
+ mGeneratedAdapter.callMethods(source, event, true, null);
+ }
+}
diff --git a/android/arch/lifecycle/TestUtils.java b/android/arch/lifecycle/TestUtils.java
index c5a520f..f0214bf 100644
--- a/android/arch/lifecycle/TestUtils.java
+++ b/android/arch/lifecycle/TestUtils.java
@@ -23,15 +23,16 @@
import android.app.Instrumentation.ActivityMonitor;
import android.support.test.InstrumentationRegistry;
import android.support.test.rule.ActivityTestRule;
+import android.support.v4.app.FragmentActivity;
import java.util.concurrent.CountDownLatch;
-public class TestUtils {
+class TestUtils {
private static final long TIMEOUT_MS = 2000;
@SuppressWarnings("unchecked")
- public static <T extends Activity> T recreateActivity(final T activity, ActivityTestRule rule)
+ static <T extends Activity> T recreateActivity(final T activity, ActivityTestRule rule)
throws Throwable {
ActivityMonitor monitor = new ActivityMonitor(
activity.getClass().getCanonicalName(), null, false);
@@ -60,7 +61,7 @@
return result;
}
- static void waitTillResumed(final LifecycleActivity a, ActivityTestRule<?> activityRule)
+ static void waitTillResumed(final FragmentActivity a, ActivityTestRule<?> activityRule)
throws Throwable {
final CountDownLatch latch = new CountDownLatch(1);
activityRule.runOnUiThread(() -> {
diff --git a/android/arch/lifecycle/Transformations.java b/android/arch/lifecycle/Transformations.java
index c316563..9ce9cbb 100644
--- a/android/arch/lifecycle/Transformations.java
+++ b/android/arch/lifecycle/Transformations.java
@@ -22,6 +22,12 @@
/**
* Transformations for a {@link LiveData} class.
+ * <p>
+ * You can use transformation methods to carry information across the observer's lifecycle. The
+ * transformations aren't calculated unless an observer is observing the returned LiveData object.
+ * <p>
+ * Because the transformations are calculated lazily, lifecycle-related behavior is implicitly
+ * passed down without requiring additional explicit calls or dependencies.
*/
@SuppressWarnings("WeakerAccess")
public class Transformations {
@@ -34,6 +40,18 @@
* LiveData and returns LiveData, which emits resulting values.
* <p>
* The given function {@code func} will be executed on the main thread.
+ * <p>
+ * Suppose that you have a LiveData, named {@code userLiveData}, that contains user data and you
+ * need to display the user name, created by concatenating the first and the last
+ * name of the user. You can define a function that handles the name creation, that will be
+ * applied to every value emitted by {@code useLiveData}.
+ *
+ * <pre>
+ * LiveData<User> userLiveData = ...;
+ * LiveData<String> userName = Transformations.map(userLiveData, user -> {
+ * return user.firstName + " " + user.lastName
+ * });
+ * </pre>
*
* @param source a {@code LiveData} to listen to
* @param func a function to apply
@@ -63,9 +81,39 @@
* <p>
* If the given function returns null, then {@code swLiveData} is not "backed" by any other
* LiveData.
+ *
* <p>
* The given function {@code func} will be executed on the main thread.
*
+ * <p>
+ * Consider the case where you have a LiveData containing a user id. Every time there's a new
+ * user id emitted, you want to trigger a request to get the user object corresponding to that
+ * id, from a repository that also returns a LiveData.
+ * <p>
+ * The {@code userIdLiveData} is the trigger and the LiveData returned by the {@code
+ * repository.getUserById} is the "backing" LiveData.
+ * <p>
+ * In a scenario where the repository contains User(1, "Jane") and User(2, "John"), when the
+ * userIdLiveData value is set to "1", the {@code switchMap} will call {@code getUser(1)},
+ * that will return a LiveData containing the value User(1, "Jane"). So now, the userLiveData
+ * will emit User(1, "Jane"). When the user in the repository gets updated to User(1, "Sarah"),
+ * the {@code userLiveData} gets automatically notified and will emit User(1, "Sarah").
+ * <p>
+ * When the {@code setUserId} method is called with userId = "2", the value of the {@code
+ * userIdLiveData} changes and automatically triggers a request for getting the user with id
+ * "2" from the repository. So, the {@code userLiveData} emits User(2, "John"). The LiveData
+ * returned by {@code repository.getUserById(1)} is removed as a source.
+ *
+ * <pre>
+ * MutableLiveData<String> userIdLiveData = ...;
+ * LiveData<User> userLiveData = Transformations.switchMap(userIdLiveData, id ->
+ * repository.getUserById(id));
+ *
+ * void setUserId(String userId) {
+ * this.userIdLiveData.setValue(userId);
+ * }
+ * </pre>
+ *
* @param trigger a {@code LiveData} to listen to
* @param func a function which creates "backing" LiveData
* @param <X> a type of {@code source} LiveData
diff --git a/android/arch/lifecycle/TransformationsTest.java b/android/arch/lifecycle/TransformationsTest.java
index e92ecca..940a3e8 100644
--- a/android/arch/lifecycle/TransformationsTest.java
+++ b/android/arch/lifecycle/TransformationsTest.java
@@ -25,7 +25,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import android.arch.core.executor.AppToolkitTaskExecutor;
+import android.arch.core.executor.ArchTaskExecutor;
import android.arch.core.util.Function;
import android.arch.lifecycle.util.InstantTaskExecutor;
@@ -42,7 +42,7 @@
@Before
public void swapExecutorDelegate() {
- AppToolkitTaskExecutor.getInstance().setDelegate(new InstantTaskExecutor());
+ ArchTaskExecutor.getInstance().setDelegate(new InstantTaskExecutor());
}
@Before
diff --git a/android/arch/lifecycle/ViewModelProviderTest.java b/android/arch/lifecycle/ViewModelProviderTest.java
index 61760fc..8877357 100644
--- a/android/arch/lifecycle/ViewModelProviderTest.java
+++ b/android/arch/lifecycle/ViewModelProviderTest.java
@@ -21,6 +21,8 @@
import static org.hamcrest.MatcherAssert.assertThat;
import android.arch.lifecycle.ViewModelProvider.NewInstanceFactory;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
import org.junit.Assert;
import org.junit.Before;
@@ -82,6 +84,18 @@
assertThat(viewModel, is(provider.get(ViewModel1.class)));
}
+ @Test(expected = IllegalStateException.class)
+ public void testNotAttachedActivity() {
+ // This is similar to call ViewModelProviders.of in Activity's constructor
+ ViewModelProviders.of(new FragmentActivity());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testNotAttachedFragment() {
+ // This is similar to call ViewModelProviders.of in Activity's constructor
+ ViewModelProviders.of(new Fragment());
+ }
+
public static class ViewModel1 extends ViewModel {
boolean mCleared;
diff --git a/android/arch/lifecycle/ViewModelProviders.java b/android/arch/lifecycle/ViewModelProviders.java
index f64365b..746162a 100644
--- a/android/arch/lifecycle/ViewModelProviders.java
+++ b/android/arch/lifecycle/ViewModelProviders.java
@@ -17,6 +17,7 @@
package android.arch.lifecycle;
import android.annotation.SuppressLint;
+import android.app.Activity;
import android.app.Application;
import android.arch.lifecycle.ViewModelProvider.Factory;
import android.support.annotation.MainThread;
@@ -40,6 +41,23 @@
}
}
+ private static Application checkApplication(Activity activity) {
+ Application application = activity.getApplication();
+ if (application == null) {
+ throw new IllegalStateException("Your activity/fragment is not yet attached to "
+ + "Application. You can't request ViewModel before onCreate call.");
+ }
+ return application;
+ }
+
+ private static Activity checkActivity(Fragment fragment) {
+ Activity activity = fragment.getActivity();
+ if (activity == null) {
+ throw new IllegalStateException("Can't create ViewModelProvider for detached fragment");
+ }
+ return activity;
+ }
+
/**
* Creates a {@link ViewModelProvider}, which retains ViewModels while a scope of given
* {@code fragment} is alive. More detailed explanation is in {@link ViewModel}.
@@ -51,12 +69,7 @@
*/
@MainThread
public static ViewModelProvider of(@NonNull Fragment fragment) {
- FragmentActivity activity = fragment.getActivity();
- if (activity == null) {
- throw new IllegalArgumentException(
- "Can't create ViewModelProvider for detached fragment");
- }
- initializeFactoryIfNeeded(activity.getApplication());
+ initializeFactoryIfNeeded(checkApplication(checkActivity(fragment)));
return new ViewModelProvider(ViewModelStores.of(fragment), sDefaultFactory);
}
@@ -71,7 +84,7 @@
*/
@MainThread
public static ViewModelProvider of(@NonNull FragmentActivity activity) {
- initializeFactoryIfNeeded(activity.getApplication());
+ initializeFactoryIfNeeded(checkApplication(activity));
return new ViewModelProvider(ViewModelStores.of(activity), sDefaultFactory);
}
@@ -87,6 +100,7 @@
*/
@MainThread
public static ViewModelProvider of(@NonNull Fragment fragment, @NonNull Factory factory) {
+ checkApplication(checkActivity(fragment));
return new ViewModelProvider(ViewModelStores.of(fragment), factory);
}
@@ -103,6 +117,7 @@
@MainThread
public static ViewModelProvider of(@NonNull FragmentActivity activity,
@NonNull Factory factory) {
+ checkApplication(activity);
return new ViewModelProvider(ViewModelStores.of(activity), factory);
}
diff --git a/android/arch/lifecycle/ViewModelTest.java b/android/arch/lifecycle/ViewModelTest.java
index 98ce027..03ebdf3 100644
--- a/android/arch/lifecycle/ViewModelTest.java
+++ b/android/arch/lifecycle/ViewModelTest.java
@@ -32,6 +32,7 @@
import android.support.test.filters.MediumTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
@@ -120,7 +121,7 @@
void onResume() {
try {
final FragmentManager manager = activity.getSupportFragmentManager();
- LifecycleFragment fragment = new LifecycleFragment();
+ Fragment fragment = new Fragment();
manager.beginTransaction().add(fragment, "temp").commitNow();
ViewModel1 vm = ViewModelProviders.of(fragment).get(ViewModel1.class);
assertThat(vm.mCleared, is(false));
diff --git a/android/arch/lifecycle/activity/EmptyActivity.java b/android/arch/lifecycle/activity/EmptyActivity.java
index 017fff4..c32c898 100644
--- a/android/arch/lifecycle/activity/EmptyActivity.java
+++ b/android/arch/lifecycle/activity/EmptyActivity.java
@@ -16,13 +16,12 @@
package android.arch.lifecycle.activity;
+import android.arch.lifecycle.extensions.test.R;
import android.os.Bundle;
import android.support.annotation.Nullable;
+import android.support.v4.app.FragmentActivity;
-import android.arch.lifecycle.LifecycleActivity;
-import android.arch.lifecycle.extensions.test.R;
-
-public class EmptyActivity extends LifecycleActivity {
+public class EmptyActivity extends FragmentActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
diff --git a/android/arch/lifecycle/activity/FragmentLifecycleActivity.java b/android/arch/lifecycle/activity/FragmentLifecycleActivity.java
index f4485e8..2eb1cc2 100644
--- a/android/arch/lifecycle/activity/FragmentLifecycleActivity.java
+++ b/android/arch/lifecycle/activity/FragmentLifecycleActivity.java
@@ -17,7 +17,6 @@
package android.arch.lifecycle.activity;
import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleFragment;
import android.arch.lifecycle.LifecycleObserver;
import android.arch.lifecycle.LifecycleOwner;
import android.arch.lifecycle.OnLifecycleEvent;
@@ -26,6 +25,7 @@
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import java.util.ArrayList;
@@ -70,9 +70,9 @@
mLoggedEvents.clear();
}
- public static class MainFragment extends LifecycleFragment {
+ public static class MainFragment extends Fragment {
@Nullable
- LifecycleFragment mNestedFragment;
+ Fragment mNestedFragment;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -85,7 +85,7 @@
}
}
- public static class NestedFragment extends LifecycleFragment {
+ public static class NestedFragment extends Fragment {
}
public static Intent intentFor(Context context, boolean nested) {
@@ -98,7 +98,8 @@
mObservedOwner = provider;
provider.getLifecycle().addObserver(new LifecycleObserver() {
@OnLifecycleEvent(Lifecycle.Event.ON_ANY)
- public void anyEvent(LifecycleOwner owner, Lifecycle.Event event) {
+ public void anyEvent(@SuppressWarnings("unused") LifecycleOwner owner,
+ Lifecycle.Event event) {
mLoggedEvents.add(event);
}
});
diff --git a/android/arch/lifecycle/observers/Base.java b/android/arch/lifecycle/observers/Base.java
new file mode 100644
index 0000000..08919d4
--- /dev/null
+++ b/android/arch/lifecycle/observers/Base.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle.observers;
+
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.LifecycleObserver;
+import android.arch.lifecycle.OnLifecycleEvent;
+
+public class Base implements LifecycleObserver {
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
+ public void onCreate() {
+ }
+}
diff --git a/android/arch/lifecycle/observers/Base_LifecycleAdapter.java b/android/arch/lifecycle/observers/Base_LifecycleAdapter.java
new file mode 100644
index 0000000..4218b96
--- /dev/null
+++ b/android/arch/lifecycle/observers/Base_LifecycleAdapter.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle.observers;
+
+import android.arch.lifecycle.GeneratedAdapter;
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.LifecycleOwner;
+import android.arch.lifecycle.MethodCallsLogger;
+
+public class Base_LifecycleAdapter implements GeneratedAdapter {
+
+ public Base_LifecycleAdapter(Base base) {
+ }
+
+ @Override
+ public void callMethods(LifecycleOwner source, Lifecycle.Event event, boolean onAny,
+ MethodCallsLogger logger) {
+
+ }
+}
diff --git a/android/arch/lifecycle/observers/DerivedSequence1.java b/android/arch/lifecycle/observers/DerivedSequence1.java
new file mode 100644
index 0000000..9db37f1
--- /dev/null
+++ b/android/arch/lifecycle/observers/DerivedSequence1.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle.observers;
+
+public class DerivedSequence1 extends Base {
+
+ public void something() {
+ }
+}
diff --git a/android/arch/lifecycle/observers/DerivedSequence2.java b/android/arch/lifecycle/observers/DerivedSequence2.java
new file mode 100644
index 0000000..f2ef943
--- /dev/null
+++ b/android/arch/lifecycle/observers/DerivedSequence2.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle.observers;
+
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.OnLifecycleEvent;
+
+public class DerivedSequence2 extends DerivedSequence1 {
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
+ void onStop() {
+
+ }
+}
diff --git a/android/arch/lifecycle/observers/DerivedWithNewMethods.java b/android/arch/lifecycle/observers/DerivedWithNewMethods.java
new file mode 100644
index 0000000..b1eaef0
--- /dev/null
+++ b/android/arch/lifecycle/observers/DerivedWithNewMethods.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle.observers;
+
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.OnLifecycleEvent;
+
+public class DerivedWithNewMethods extends Base {
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
+ void onStop() {
+
+ }
+}
diff --git a/android/arch/lifecycle/observers/DerivedWithNoNewMethods.java b/android/arch/lifecycle/observers/DerivedWithNoNewMethods.java
new file mode 100644
index 0000000..cb1afb8
--- /dev/null
+++ b/android/arch/lifecycle/observers/DerivedWithNoNewMethods.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle.observers;
+
+public class DerivedWithNoNewMethods extends Base {
+}
diff --git a/android/arch/lifecycle/observers/DerivedWithOverridenMethodsWithLfAnnotation.java b/android/arch/lifecycle/observers/DerivedWithOverridenMethodsWithLfAnnotation.java
new file mode 100644
index 0000000..40c7c9a
--- /dev/null
+++ b/android/arch/lifecycle/observers/DerivedWithOverridenMethodsWithLfAnnotation.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle.observers;
+
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.OnLifecycleEvent;
+
+public class DerivedWithOverridenMethodsWithLfAnnotation extends Base {
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ }
+}
diff --git a/android/arch/lifecycle/observers/Interface1.java b/android/arch/lifecycle/observers/Interface1.java
new file mode 100644
index 0000000..e193de9
--- /dev/null
+++ b/android/arch/lifecycle/observers/Interface1.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle.observers;
+
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.LifecycleObserver;
+import android.arch.lifecycle.OnLifecycleEvent;
+
+public interface Interface1 extends LifecycleObserver {
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
+ void onCreate();
+}
diff --git a/android/arch/lifecycle/observers/Interface1_LifecycleAdapter.java b/android/arch/lifecycle/observers/Interface1_LifecycleAdapter.java
new file mode 100644
index 0000000..c597b1c
--- /dev/null
+++ b/android/arch/lifecycle/observers/Interface1_LifecycleAdapter.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle.observers;
+
+import android.arch.lifecycle.GeneratedAdapter;
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.LifecycleOwner;
+import android.arch.lifecycle.MethodCallsLogger;
+
+public class Interface1_LifecycleAdapter implements GeneratedAdapter {
+
+ public Interface1_LifecycleAdapter(Interface1 base) {
+ }
+
+ @Override
+ public void callMethods(LifecycleOwner source, Lifecycle.Event event, boolean onAny,
+ MethodCallsLogger logger) {
+
+ }
+}
diff --git a/android/arch/lifecycle/observers/Interface2.java b/android/arch/lifecycle/observers/Interface2.java
new file mode 100644
index 0000000..1056fcb
--- /dev/null
+++ b/android/arch/lifecycle/observers/Interface2.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle.observers;
+
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.LifecycleObserver;
+import android.arch.lifecycle.OnLifecycleEvent;
+
+public interface Interface2 extends LifecycleObserver {
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
+ void onCreate();
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+ void onDestroy();
+}
diff --git a/android/arch/lifecycle/observers/Interface2_LifecycleAdapter.java b/android/arch/lifecycle/observers/Interface2_LifecycleAdapter.java
new file mode 100644
index 0000000..b05b41a
--- /dev/null
+++ b/android/arch/lifecycle/observers/Interface2_LifecycleAdapter.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle.observers;
+
+import android.arch.lifecycle.GeneratedAdapter;
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.LifecycleOwner;
+import android.arch.lifecycle.MethodCallsLogger;
+
+public class Interface2_LifecycleAdapter implements GeneratedAdapter {
+
+ public Interface2_LifecycleAdapter(Interface2 base) {
+ }
+
+ @Override
+ public void callMethods(LifecycleOwner source, Lifecycle.Event event, boolean onAny,
+ MethodCallsLogger logger) {
+
+ }
+}
diff --git a/android/arch/lifecycle/observers/InterfaceImpl1.java b/android/arch/lifecycle/observers/InterfaceImpl1.java
new file mode 100644
index 0000000..2f03393
--- /dev/null
+++ b/android/arch/lifecycle/observers/InterfaceImpl1.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle.observers;
+
+public class InterfaceImpl1 implements Interface1 {
+ @Override
+ public void onCreate() {
+ }
+}
diff --git a/android/arch/lifecycle/observers/InterfaceImpl2.java b/android/arch/lifecycle/observers/InterfaceImpl2.java
new file mode 100644
index 0000000..eef8ce4
--- /dev/null
+++ b/android/arch/lifecycle/observers/InterfaceImpl2.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle.observers;
+
+public class InterfaceImpl2 implements Interface1, Interface2 {
+ @Override
+ public void onCreate() {
+ }
+
+ @Override
+ public void onDestroy() {
+
+ }
+}
diff --git a/android/arch/lifecycle/observers/InterfaceImpl3.java b/android/arch/lifecycle/observers/InterfaceImpl3.java
new file mode 100644
index 0000000..8f31808
--- /dev/null
+++ b/android/arch/lifecycle/observers/InterfaceImpl3.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle.observers;
+
+public class InterfaceImpl3 extends Base implements Interface1 {
+ @Override
+ public void onCreate() {
+ }
+}
diff --git a/android/arch/lifecycle/testapp/FullLifecycleTestActivity.java b/android/arch/lifecycle/testapp/FullLifecycleTestActivity.java
index 5972b16..5f33c28 100644
--- a/android/arch/lifecycle/testapp/FullLifecycleTestActivity.java
+++ b/android/arch/lifecycle/testapp/FullLifecycleTestActivity.java
@@ -19,8 +19,8 @@
import static android.arch.lifecycle.testapp.TestEvent.ACTIVITY_CALLBACK;
import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleActivity;
import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
import android.util.Pair;
import java.util.ArrayList;
@@ -31,7 +31,7 @@
/**
* Activity for testing full lifecycle
*/
-public class FullLifecycleTestActivity extends LifecycleActivity implements CollectingActivity {
+public class FullLifecycleTestActivity extends FragmentActivity implements CollectingActivity {
private List<Pair<TestEvent, Lifecycle.Event>> mCollectedEvents = new ArrayList<>();
private TestObserver mTestObserver = new TestObserver(mCollectedEvents);
diff --git a/android/arch/lifecycle/testapp/LifecycleTestActivity.java b/android/arch/lifecycle/testapp/LifecycleTestActivity.java
index 093ec7f..cf07aee 100644
--- a/android/arch/lifecycle/testapp/LifecycleTestActivity.java
+++ b/android/arch/lifecycle/testapp/LifecycleTestActivity.java
@@ -16,13 +16,13 @@
package android.arch.lifecycle.testapp;
-import android.arch.lifecycle.LifecycleActivity;
import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
/**
* Activity for testing events by themselves
*/
-public class LifecycleTestActivity extends LifecycleActivity {
+public class LifecycleTestActivity extends FragmentActivity {
/**
* identifies that
diff --git a/android/arch/lifecycle/testapp/NavigationDialogActivity.java b/android/arch/lifecycle/testapp/NavigationDialogActivity.java
index 709bd8d..0ae9403 100644
--- a/android/arch/lifecycle/testapp/NavigationDialogActivity.java
+++ b/android/arch/lifecycle/testapp/NavigationDialogActivity.java
@@ -16,10 +16,10 @@
package android.arch.lifecycle.testapp;
-import android.arch.lifecycle.LifecycleActivity;
+import android.support.v4.app.FragmentActivity;
/**
* an activity with Dialog theme.
*/
-public class NavigationDialogActivity extends LifecycleActivity {
+public class NavigationDialogActivity extends FragmentActivity {
}
diff --git a/android/arch/lifecycle/testapp/NavigationTestActivityFirst.java b/android/arch/lifecycle/testapp/NavigationTestActivityFirst.java
index f1847c9..69fd478 100644
--- a/android/arch/lifecycle/testapp/NavigationTestActivityFirst.java
+++ b/android/arch/lifecycle/testapp/NavigationTestActivityFirst.java
@@ -16,10 +16,10 @@
package android.arch.lifecycle.testapp;
-import android.arch.lifecycle.LifecycleActivity;
+import android.support.v4.app.FragmentActivity;
/**
* Activity for ProcessOwnerTest
*/
-public class NavigationTestActivityFirst extends LifecycleActivity {
+public class NavigationTestActivityFirst extends FragmentActivity {
}
diff --git a/android/arch/lifecycle/testapp/NavigationTestActivitySecond.java b/android/arch/lifecycle/testapp/NavigationTestActivitySecond.java
index 221e927..0f9a4c9 100644
--- a/android/arch/lifecycle/testapp/NavigationTestActivitySecond.java
+++ b/android/arch/lifecycle/testapp/NavigationTestActivitySecond.java
@@ -16,10 +16,10 @@
package android.arch.lifecycle.testapp;
-import android.arch.lifecycle.LifecycleActivity;
+import android.support.v4.app.FragmentActivity;
/**
* Activity for ProcessOwnerTest
*/
-public class NavigationTestActivitySecond extends LifecycleActivity {
+public class NavigationTestActivitySecond extends FragmentActivity {
}
diff --git a/android/arch/lifecycle/testapp/SimpleAppLifecycleTestActivity.java b/android/arch/lifecycle/testapp/SimpleAppLifecycleTestActivity.java
index 6d61c5e..77bd99f 100644
--- a/android/arch/lifecycle/testapp/SimpleAppLifecycleTestActivity.java
+++ b/android/arch/lifecycle/testapp/SimpleAppLifecycleTestActivity.java
@@ -17,12 +17,12 @@
package android.arch.lifecycle.testapp;
import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleActivity;
import android.arch.lifecycle.LifecycleObserver;
import android.arch.lifecycle.LifecycleOwner;
import android.arch.lifecycle.OnLifecycleEvent;
import android.arch.lifecycle.ProcessLifecycleOwner;
import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
import android.util.Pair;
import java.util.ArrayList;
@@ -33,7 +33,7 @@
/**
* Activity for SimpleAppFullLifecycleTest
*/
-public class SimpleAppLifecycleTestActivity extends LifecycleActivity {
+public class SimpleAppLifecycleTestActivity extends FragmentActivity {
public enum TestEventType {
PROCESS_EVENT,
diff --git a/android/arch/lifecycle/viewmodeltest/ViewModelActivity.java b/android/arch/lifecycle/viewmodeltest/ViewModelActivity.java
index 5ef9f16..1f9f100 100644
--- a/android/arch/lifecycle/viewmodeltest/ViewModelActivity.java
+++ b/android/arch/lifecycle/viewmodeltest/ViewModelActivity.java
@@ -16,15 +16,14 @@
package android.arch.lifecycle.viewmodeltest;
+import android.arch.lifecycle.ViewModelProviders;
import android.arch.lifecycle.extensions.test.R;
import android.os.Bundle;
import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
-import android.arch.lifecycle.LifecycleActivity;
-import android.arch.lifecycle.LifecycleFragment;
-import android.arch.lifecycle.ViewModelProviders;
-
-public class ViewModelActivity extends LifecycleActivity {
+public class ViewModelActivity extends FragmentActivity {
public static final String KEY_FRAGMENT_MODEL = "fragment-model";
public static final String KEY_ACTIVITY_MODEL = "activity-model";
public static final String FRAGMENT_TAG_1 = "f1";
@@ -47,7 +46,7 @@
defaultActivityModel = ViewModelProviders.of(this).get(TestViewModel.class);
}
- public static class ViewModelFragment extends LifecycleFragment {
+ public static class ViewModelFragment extends Fragment {
public TestViewModel fragmentModel;
public TestViewModel activityModel;
public TestViewModel defaultActivityModel;
diff --git a/android/arch/paging/BoundedDataSource.java b/android/arch/paging/BoundedDataSource.java
index 96c23fc..664ab16 100644
--- a/android/arch/paging/BoundedDataSource.java
+++ b/android/arch/paging/BoundedDataSource.java
@@ -18,6 +18,7 @@
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
+import android.support.annotation.WorkerThread;
import java.util.ArrayList;
import java.util.Collections;
@@ -49,15 +50,18 @@
* @return List of loaded items. Null if the BoundedDataSource is no longer valid, and should
* not be queried again.
*/
+ @WorkerThread
@Nullable
public abstract List<Value> loadRange(int startPosition, int loadCount);
+ @WorkerThread
@Nullable
@Override
public List<Value> loadAfter(int startIndex, int pageSize) {
return loadRange(startIndex, pageSize);
}
+ @WorkerThread
@Nullable
@Override
public List<Value> loadBefore(int startIndex, int pageSize) {
diff --git a/android/arch/paging/ContiguousDataSource.java b/android/arch/paging/ContiguousDataSource.java
index 9ff1117..afcc208 100644
--- a/android/arch/paging/ContiguousDataSource.java
+++ b/android/arch/paging/ContiguousDataSource.java
@@ -41,6 +41,8 @@
return true;
}
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@WorkerThread
@Nullable
public abstract NullPaddedList<Value> loadInitial(
@@ -58,7 +60,10 @@
* @param pageSize Suggested number of items to load.
* @return List of items, starting at position currentEndIndex + 1. Null if the data source is
* no longer valid, and should not be queried again.
+ *
+ * @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@WorkerThread
@Nullable
public final List<Value> loadAfter(int currentEndIndex,
@@ -88,7 +93,10 @@
* on item contents.
* @param pageSize Suggested number of items to load.
* @return List of items, in descending order, starting at position currentBeginIndex - 1.
+ *
+ * @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@WorkerThread
@Nullable
public final List<Value> loadBefore(int currentBeginIndex,
diff --git a/android/arch/paging/DataSource.java b/android/arch/paging/DataSource.java
index 1e81569..48fbec5 100644
--- a/android/arch/paging/DataSource.java
+++ b/android/arch/paging/DataSource.java
@@ -17,6 +17,7 @@
package android.arch.paging;
import android.support.annotation.AnyThread;
+import android.support.annotation.WorkerThread;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -64,6 +65,7 @@
* @return number of items that this DataSource can provide in total, or
* {@link #COUNT_UNDEFINED} if expensive or undesired to compute.
*/
+ @WorkerThread
public abstract int countItems();
/**
@@ -143,6 +145,7 @@
*
* @return True if the data source is invalid, and can no longer return data.
*/
+ @WorkerThread
public boolean isInvalid() {
return mInvalid.get();
}
diff --git a/android/arch/paging/KeyedDataSource.java b/android/arch/paging/KeyedDataSource.java
index 057eb7f..8cf6829 100644
--- a/android/arch/paging/KeyedDataSource.java
+++ b/android/arch/paging/KeyedDataSource.java
@@ -16,6 +16,7 @@
package android.arch.paging;
+import android.support.annotation.AnyThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
@@ -54,7 +55,7 @@
* {@literal @}SuppressWarnings("FieldCanBeLocal")
* private final InvalidationTracker.Observer mObserver;
*
- * public OffsetUserQueryDataSource(MyDatabase db) {
+ * public KeyedUserQueryDataSource(MyDatabase db) {
* mDb = db;
* mUserDao = db.getUserDao();
* mObserver = new InvalidationTracker.Observer("user") {
@@ -85,11 +86,15 @@
*
* {@literal @}Override
* public List<User> loadBefore({@literal @}NonNull String userName, int pageSize) {
+ * // Return items adjacent to 'userName' in reverse order
+ * // it's valid to return a different-sized list of items than pageSize, if it's easier
* return mUserDao.userNameLoadBefore(userName, pageSize);
* }
*
* {@literal @}Override
* public List<User> loadAfter({@literal @}Nullable String userName, int pageSize) {
+ * // Return items adjacent to 'userName'
+ * // it's valid to return a different-sized list of items than pageSize, if it's easier
* return mUserDao.userNameLoadAfter(userName, pageSize);
* }
* }</pre>
@@ -198,13 +203,64 @@
return list;
}
+ /**
+ * Return a key associated with the given item.
+ * <p>
+ * If your KeyedDataSource is loading from a source that is sorted and loaded by a unique
+ * integer ID, you would return {@code item.getID()} here. This key can then be passed to
+ * {@link #loadBefore(Key, int)} or {@link #loadAfter(Key, int)} to load additional items
+ * adjacent to the item passed to this function.
+ * <p>
+ * If your key is more complex, such as when you're sorting by name, then resolving collisions
+ * with integer ID, you'll need to return both. In such a case you would use a wrapper class,
+ * such as {@code Pair<String, Integer>} or, in Kotlin,
+ * {@code data class Key(val name: String, val id: Int)}
+ *
+ * @param item Item to get the key from.
+ * @return Key associated with given item.
+ */
@NonNull
+ @AnyThread
public abstract Key getKey(@NonNull Value item);
+ /**
+ * Return the number of items that occur before the item uniquely identified by {@code key} in
+ * the data set.
+ * <p>
+ * For example, if you're loading items sorted by ID, then this would return the total number of
+ * items with ID less than {@code key}.
+ * <p>
+ * If you return {@link #COUNT_UNDEFINED} here, or from {@link #countItemsAfter(Key)}, your
+ * data source will not present placeholder null items in place of unloaded data.
+ *
+ * @param key A unique identifier of an item in the data set.
+ * @return Number of items in the data set before the item identified by {@code key}, or
+ * {@link #COUNT_UNDEFINED}.
+ *
+ * @see #countItemsAfter(Key)
+ */
+ @WorkerThread
public int countItemsBefore(@NonNull Key key) {
return COUNT_UNDEFINED;
}
+ /**
+ * Return the number of items that occur after the item uniquely identified by {@code key} in
+ * the data set.
+ * <p>
+ * For example, if you're loading items sorted by ID, then this would return the total number of
+ * items with ID greater than {@code key}.
+ * <p>
+ * If you return {@link #COUNT_UNDEFINED} here, or from {@link #countItemsBefore(Key)}, your
+ * data source will not present placeholder null items in place of unloaded data.
+ *
+ * @param key A unique identifier of an item in the data set.
+ * @return Number of items in the data set after the item identified by {@code key}, or
+ * {@link #COUNT_UNDEFINED}.
+ *
+ * @see #countItemsBefore(Key)
+ */
+ @WorkerThread
public int countItemsAfter(@NonNull Key key) {
return COUNT_UNDEFINED;
}
@@ -231,10 +287,17 @@
public abstract List<Value> loadAfter(@NonNull Key currentEndKey, int pageSize);
/**
- * Load data before the currently loaded content, starting at the provided index.
+ * Load data before the currently loaded content, starting at the provided index,
+ * in reverse-display order.
* <p>
* It's valid to return a different list size than the page size, if it's easier for this data
* source. It is generally safer to increase the number loaded than reduce.
+ * <p class="note"><strong>Note:</strong> Items returned from loadBefore <em>must</em> be in
+ * reverse order from how they will be presented in the list. The first item in the return list
+ * will be prepended immediately before the current beginning of the list. This is so that the
+ * KeyedDataSource may return a different number of items from the requested {@code pageSize} by
+ * shortening or lengthening the return list as it desires.
+ * <p>
*
* @param currentBeginKey Load items before this key.
* @param pageSize Suggested number of items to load.
diff --git a/android/arch/paging/PagedList.java b/android/arch/paging/PagedList.java
index 0d5313d..6a31b68 100644
--- a/android/arch/paging/PagedList.java
+++ b/android/arch/paging/PagedList.java
@@ -27,16 +27,65 @@
/**
* Lazy loading list that pages in content from a {@link DataSource}.
* <p>
- * A PagedList is a lazy loaded list, which presents data from a {@link DataSource}. If the
- * DataSource is counted (returns a valid number from its count method(s)), the PagedList will
- * present {@code null} items in place of not-yet-loaded content to serve as placeholders.
+ * A PagedList is a {@link List} which loads its data in chunks (pages) from a {@link DataSource}.
+ * Items can be accessed with {@link #get(int)}, and further loading can be triggered with
+ * {@link #loadAround(int)}. See {@link PagedListAdapter}, which enables the binding of a PagedList
+ * to a {@link android.support.v7.widget.RecyclerView}.
+ * <h4>Loading Data</h4>
* <p>
- * When {@link #loadAround} is called, items will be loaded in near the passed position. If
- * placeholder {@code null}s are present in the list, they will be replaced as content is loaded.
+ * All data in a PagedList is loaded from its {@link DataSource}. Creating a PagedList loads data
+ * from the DataSource immediately, and should for this reason be done on a background thread. The
+ * constructed PagedList may then be passed to and used on the UI thread. This is done to prevent
+ * passing a list with no loaded content to the UI thread, which should generally not be presented
+ * to the user.
* <p>
- * In this way, PagedList can present data for an unbounded, infinite scrolling list, or a very
- * large but countable list. See {@link PagedListAdapter}, which enables the binding of a PagedList
- * to a RecyclerView. Use {@link Config} to control how many items a PagedList loads, and when.
+ * When {@link #loadAround} is called, items will be loaded in near the passed list index. If
+ * placeholder {@code null}s are present in the list, they will be replaced as content is
+ * loaded. If not, newly loaded items will be inserted at the beginning or end of the list.
+ * <p>
+ * PagedList can present data for an unbounded, infinite scrolling list, or a very large but
+ * countable list. Use {@link Config} to control how many items a PagedList loads, and when.
+ * <p>
+ * If you use {@link LivePagedListProvider} to get a
+ * {@link android.arch.lifecycle.LiveData}<PagedList>, it will initialize PagedLists on a
+ * background thread for you.
+ * <h4>Placeholders</h4>
+ * <p>
+ * There are two ways that PagedList can represent its not-yet-loaded data - with or without
+ * {@code null} placeholders.
+ * <p>
+ * With placeholders, the PagedList is always the full size of the data set. {@code get(N)} returns
+ * the {@code N}th item in the data set, or {@code null} if its not yet loaded.
+ * <p>
+ * Without {@code null} placeholders, the PagedList is the sublist of data that has already been
+ * loaded. The size of the PagedList is the number of currently loaded items, and {@code get(N)}
+ * returns the {@code N}th <em>loaded</em> item. This is not necessarily the {@code N}th item in the
+ * data set.
+ * <p>
+ * Placeholders have several benefits:
+ * <ul>
+ * <li>They express the full sized list to the presentation layer (often a
+ * {@link PagedListAdapter}), and so can support scrollbars (without jumping as pages are
+ * loaded) and fast-scrolling to any position, whether loaded or not.
+ * <li>They avoid the need for a loading spinner at the end of the loaded list, since the list
+ * is always full sized.
+ * </ul>
+ * <p>
+ * They also have drawbacks:
+ * <ul>
+ * <li>Your Adapter (or other presentation mechanism) needs to account for {@code null} items.
+ * This often means providing default values in data you bind to a
+ * {@link android.support.v7.widget.RecyclerView.ViewHolder}.
+ * <li>They don't work well if your item views are of different sizes, as this will prevent
+ * loading items from cross-fading nicely.
+ * <li>They require you to count your data set, which can be expensive or impossible, depending
+ * on where your data comes from.
+ * </ul>
+ * <p>
+ * Placeholders are enabled by default, but can be disabled in two ways. They are disabled if the
+ * DataSource returns {@link DataSource#COUNT_UNDEFINED} from any item counting method, or if
+ * {@code false} is passed to {@link Config.Builder#setEnablePlaceholders(boolean)} when building a
+ * {@link Config}.
*
* @param <T> The type of the entries in the list.
*/
@@ -92,6 +141,18 @@
* Builder class for PagedList.
* <p>
* DataSource, main thread and background executor, and Config must all be provided.
+ * <p>
+ * A valid PagedList may not be constructed without data, so building a PagedList queries
+ * initial data from the data source. This is done because it's generally undesired to present a
+ * PagedList with no data in it to the UI. It's better to present initial data, so that the UI
+ * doesn't show an empty list, or placeholders for a few frames, just before showing initial
+ * content.
+ * <p>
+ * Because PagedLists are initialized with data, PagedLists must be built on a background
+ * thread.
+ * <p>
+ * {@link LivePagedListProvider} does this creation on a background thread automatically, if you
+ * want to receive a {@code LiveData<PagedList<...>>}.
*
* @param <Key> Type of key used to load data from the DataSource.
* @param <Value> Type of items held and loaded by the PagedList.
@@ -174,11 +235,17 @@
* <p>
* This call will initial data and perform any counting needed to initialize the PagedList,
* therefore it should only be called on a worker thread.
+ * <p>
+ * While build() will always return a PagedList, it's important to note that the PagedList
+ * initial load may fail to acquire data from the DataSource. This can happen for example if
+ * the DataSource is invalidated during its initial load. If this happens, the PagedList
+ * will be immediately {@link PagedList#isDetached() detached}, and you can retry
+ * construction (including setting a new DataSource).
*
* @return The newly constructed PagedList
*/
- @NonNull
@WorkerThread
+ @NonNull
public PagedList<Value> build() {
if (mDataSource == null) {
throw new IllegalArgumentException("DataSource required");
@@ -403,6 +470,16 @@
* Defines the number of items loaded at once from the DataSource.
* <p>
* Should be several times the number of visible items onscreen.
+ * <p>
+ * Configuring your page size depends on how your data is being loaded and used. Smaller
+ * page sizes improve memory usage, latency, and avoid GC churn. Larger pages generally
+ * improve loading throughput, to a point
+ * (avoid loading more than 2MB from SQLite at once, since it incurs extra cost).
+ * <p>
+ * If you're loading data for very large, social-media style cards that take up most of
+ * a screen, and your database isn't a bottleneck, 10-20 may make sense. If you're
+ * displaying dozens of items in a tiled grid, which can present items during a scroll
+ * much more quickly, consider closer to 100.
*
* @param pageSize Number of items loaded at once from the DataSource.
* @return this
@@ -414,12 +491,15 @@
/**
* Defines how far from the edge of loaded content an access must be to trigger further
- * loading. Defaults to page size.
- * <p>
- * A value of 0 indicates that no list items will be loaded before they are first
- * requested.
+ * loading.
* <p>
* Should be several times the number of visible items onscreen.
+ * <p>
+ * If not set, defaults to page size.
+ * <p>
+ * A value of 0 indicates that no list items will be loaded until they are specifically
+ * requested. This is generally not recommended, so that users don't observe a
+ * placeholder item (with placeholders) or end of list (without) while scrolling.
*
* @param prefetchDistance Distance the PagedList should prefetch.
* @return this
@@ -432,8 +512,10 @@
/**
* Pass false to disable null placeholders in PagedLists using this Config.
* <p>
- * A PagedList will present null placeholders for not yet loaded content if two
- * contitions are met:
+ * If not set, defaults to true.
+ * <p>
+ * A PagedList will present null placeholders for not-yet-loaded content if two
+ * conditions are met:
* <p>
* 1) Its DataSource can count all unloaded items (so that the number of nulls to
* present is known).
@@ -442,6 +524,13 @@
* <p>
* Call {@code setEnablePlaceholders(false)} to ensure the receiver of the PagedList
* (often a {@link PagedListAdapter}) doesn't need to account for null items.
+ * <p>
+ * If placeholders are disabled, not-yet-loaded content will not be present in the list.
+ * Paging will still occur, but as items are loaded or removed, they will be signaled
+ * as inserts to the {@link PagedList.Callback}.
+ * {@link PagedList.Callback#onChanged(int, int)} will not be issued as part of loading,
+ * though a {@link PagedListAdapter} may still receive change events as a result of
+ * PagedList diffing.
*
* @param enablePlaceholders False if null placeholders should be disabled.
* @return this
diff --git a/android/arch/paging/PagedListAdapter.java b/android/arch/paging/PagedListAdapter.java
index 19a0c55..93c02ea 100644
--- a/android/arch/paging/PagedListAdapter.java
+++ b/android/arch/paging/PagedListAdapter.java
@@ -51,6 +51,7 @@
* public final LiveData<PagedList<User>> usersList;
* public MyViewModel(UserDao userDao) {
* usersList = userDao.usersByLastName().create(
+ * /* initial load position {@literal *}/ 0,
* new PagedList.Config.Builder()
* .setPageSize(50)
* .setPrefetchDistance(50)
@@ -72,7 +73,7 @@
*
* class UserAdapter extends PagedListAdapter<User, UserViewHolder> {
* public UserAdapter() {
- * super(User.DIFF_CALLBACK);
+ * super(DIFF_CALLBACK);
* }
* {@literal @}Override
* public void onBindViewHolder(UserViewHolder holder, int position) {
@@ -85,27 +86,21 @@
* holder.clear();
* }
* }
- * }
- *
- * {@literal @}Entity
- * class User {
- * // ... simple POJO code omitted ...
- *
- * public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<Customer>() {
- * {@literal @}Override
- * public boolean areItemsTheSame(
- * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
- * // User properties may have changed if reloaded from the DB, but ID is fixed
- * return oldUser.getId() == newUser.getId();
- * }
- * {@literal @}Override
- * public boolean areContentsTheSame(
- * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
- * // NOTE: if you use equals, your object must properly override Object#equals()
- * // Incorrectly returning false here will result in too many animations.
- * return oldUser.equals(newUser);
- * }
- * }
+ * public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<User>() {
+ * {@literal @}Override
+ * public boolean areItemsTheSame(
+ * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
+ * // User properties may have changed if reloaded from the DB, but ID is fixed
+ * return oldUser.getId() == newUser.getId();
+ * }
+ * {@literal @}Override
+ * public boolean areContentsTheSame(
+ * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
+ * // NOTE: if you use equals, your object must properly override Object#equals()
+ * // Incorrectly returning false here will result in too many animations.
+ * return oldUser.equals(newUser);
+ * }
+ * }
* }</pre>
*
* Advanced users that wish for more control over adapter behavior, or to provide a specific base
diff --git a/android/arch/paging/PagedListAdapterHelper.java b/android/arch/paging/PagedListAdapterHelper.java
index 4bff5fc..c7b61d9 100644
--- a/android/arch/paging/PagedListAdapterHelper.java
+++ b/android/arch/paging/PagedListAdapterHelper.java
@@ -57,6 +57,7 @@
* public final LiveData<PagedList<User>> usersList;
* public MyViewModel(UserDao userDao) {
* usersList = userDao.usersByLastName().create(
+ * /* initial load position {@literal *}/ 0,
* new PagedList.Config.Builder()
* .setPageSize(50)
* .setPrefetchDistance(50)
@@ -79,7 +80,7 @@
* class UserAdapter extends RecyclerView.Adapter<UserViewHolder> {
* private final PagedListAdapterHelper<User> mHelper;
* public UserAdapter(PagedListAdapterHelper.Builder<User> builder) {
- * mHelper = new PagedListAdapterHelper(this, User.DIFF_CALLBACK);
+ * mHelper = new PagedListAdapterHelper(this, DIFF_CALLBACK);
* }
* {@literal @}Override
* public int getItemCount() {
@@ -99,13 +100,7 @@
* holder.clear();
* }
* }
- * }
- *
- * {@literal @}Entity
- * class User {
- * // ... simple POJO code omitted ...
- *
- * public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<Customer>() {
+ * public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<User>() {
* {@literal @}Override
* public boolean areItemsTheSame(
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
diff --git a/android/arch/paging/TestExecutor.java b/android/arch/paging/TestExecutor.java
index 976f7df..30809c3 100644
--- a/android/arch/paging/TestExecutor.java
+++ b/android/arch/paging/TestExecutor.java
@@ -30,7 +30,7 @@
mTasks.add(command);
}
- public boolean executeAll() {
+ boolean executeAll() {
boolean consumed = !mTasks.isEmpty();
Runnable task;
while ((task = mTasks.poll()) != null) {
diff --git a/android/arch/paging/TiledDataSource.java b/android/arch/paging/TiledDataSource.java
index 56556cd..36be423 100644
--- a/android/arch/paging/TiledDataSource.java
+++ b/android/arch/paging/TiledDataSource.java
@@ -17,6 +17,7 @@
package android.arch.paging;
import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
import java.util.List;
@@ -90,6 +91,7 @@
*
* @return Number of items this DataSource can provide. Must be <code>0</code> or greater.
*/
+ @WorkerThread
@Override
public abstract int countItems();
@@ -100,14 +102,20 @@
/**
* Called to load items at from the specified position range.
+ * <p>
+ * This method must return a list of requested size, unless at the end of list. Fixed size pages
+ * enable TiledDataSource to navigate tiles efficiently, and quickly accesss any position in the
+ * data set.
+ * <p>
+ * If a list of a different size is returned, but it is not the last list in the data set based
+ * on the return value from {@link #countItems()}, an exception will be thrown.
*
* @param startPosition Index of first item to load.
- * @param count Exact number of items to load. Returning a different number will cause
- * an exception to be thrown.
- * @return List of loaded items. Null if the DataSource is no longer valid, and should
- * not be queried again.
+ * @param count Number of items to load.
+ * @return List of loaded items, of the requested length unless at end of list. Null if the
+ * DataSource is no longer valid, and should not be queried again.
*/
- @SuppressWarnings("WeakerAccess")
+ @WorkerThread
public abstract List<Type> loadRange(int startPosition, int count);
final List<Type> loadRangeWrapper(int startPosition, int count) {
@@ -132,6 +140,7 @@
mTiledDataSource = tiledDataSource;
}
+ @WorkerThread
@Nullable
@Override
public List<Value> loadRange(int startPosition, int loadCount) {
diff --git a/android/arch/persistence/db/SupportSQLiteOpenHelper.java b/android/arch/persistence/db/SupportSQLiteOpenHelper.java
index 5a96e5a..02e4e7d 100644
--- a/android/arch/persistence/db/SupportSQLiteOpenHelper.java
+++ b/android/arch/persistence/db/SupportSQLiteOpenHelper.java
@@ -17,13 +17,18 @@
package android.arch.persistence.db;
import android.content.Context;
-import android.database.DatabaseErrorHandler;
-import android.database.DefaultDatabaseErrorHandler;
+import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
/**
* An interface to map the behavior of {@link android.database.sqlite.SQLiteOpenHelper}.
@@ -99,10 +104,29 @@
void close();
/**
- * Matching callback methods from {@link android.database.sqlite.SQLiteOpenHelper}.
+ * Handles various lifecycle events for the SQLite connection, similar to
+ * {@link android.database.sqlite.SQLiteOpenHelper}.
*/
@SuppressWarnings({"unused", "WeakerAccess"})
abstract class Callback {
+ private static final String TAG = "SupportSQLite";
+ /**
+ * Version number of the database (starting at 1); if the database is older,
+ * {@link SupportSQLiteOpenHelper.Callback#onUpgrade(SupportSQLiteDatabase, int, int)}
+ * will be used to upgrade the database; if the database is newer,
+ * {@link SupportSQLiteOpenHelper.Callback#onDowngrade(SupportSQLiteDatabase, int, int)}
+ * will be used to downgrade the database.
+ */
+ public final int version;
+
+ /**
+ * Creates a new Callback to get database lifecycle events.
+ * @param version The version for the database instance. See {@link #version}.
+ */
+ public Callback(int version) {
+ this.version = version;
+ }
+
/**
* Called when the database connection is being configured, to enable features such as
* write-ahead logging or foreign key support.
@@ -193,6 +217,81 @@
public void onOpen(SupportSQLiteDatabase db) {
}
+
+ /**
+ * The method invoked when database corruption is detected. Default implementation will
+ * delete the database file.
+ *
+ * @param db the {@link SupportSQLiteDatabase} object representing the database on which
+ * corruption is detected.
+ */
+ public void onCorruption(SupportSQLiteDatabase db) {
+ // the following implementation is taken from {@link DefaultDatabaseErrorHandler}.
+
+ Log.e(TAG, "Corruption reported by sqlite on database: " + db.getPath());
+ // is the corruption detected even before database could be 'opened'?
+ if (!db.isOpen()) {
+ // database files are not even openable. delete this database file.
+ // NOTE if the database has attached databases, then any of them could be corrupt.
+ // and not deleting all of them could cause corrupted database file to remain and
+ // make the application crash on database open operation. To avoid this problem,
+ // the application should provide its own {@link DatabaseErrorHandler} impl class
+ // to delete ALL files of the database (including the attached databases).
+ deleteDatabaseFile(db.getPath());
+ return;
+ }
+
+ List<Pair<String, String>> attachedDbs = null;
+ try {
+ // Close the database, which will cause subsequent operations to fail.
+ // before that, get the attached database list first.
+ try {
+ attachedDbs = db.getAttachedDbs();
+ } catch (SQLiteException e) {
+ /* ignore */
+ }
+ try {
+ db.close();
+ } catch (IOException e) {
+ /* ignore */
+ }
+ } finally {
+ // Delete all files of this corrupt database and/or attached databases
+ if (attachedDbs != null) {
+ for (Pair<String, String> p : attachedDbs) {
+ deleteDatabaseFile(p.second);
+ }
+ } else {
+ // attachedDbs = null is possible when the database is so corrupt that even
+ // "PRAGMA database_list;" also fails. delete the main database file
+ deleteDatabaseFile(db.getPath());
+ }
+ }
+ }
+
+ private void deleteDatabaseFile(String fileName) {
+ if (fileName.equalsIgnoreCase(":memory:") || fileName.trim().length() == 0) {
+ return;
+ }
+ Log.w(TAG, "deleting the database file: " + fileName);
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ SQLiteDatabase.deleteDatabase(new File(fileName));
+ } else {
+ try {
+ final boolean deleted = new File(fileName).delete();
+ if (!deleted) {
+ Log.e(TAG, "Could not delete the database file " + fileName);
+ }
+ } catch (Exception error) {
+ Log.e(TAG, "error while deleting corrupted database file", error);
+ }
+ }
+ } catch (Exception e) {
+ /* print warning and ignore exception */
+ Log.w(TAG, "delete failed: ", e);
+ }
+ }
}
/**
@@ -211,33 +310,15 @@
@Nullable
public final String name;
/**
- * Version number of the database (starting at 1); if the database is older,
- * {@link SupportSQLiteOpenHelper.Callback#onUpgrade(SupportSQLiteDatabase, int, int)}
- * will be used to upgrade the database; if the database is newer,
- * {@link SupportSQLiteOpenHelper.Callback#onDowngrade(SupportSQLiteDatabase, int, int)}
- * will be used to downgrade the database.
- */
- public final int version;
- /**
* The callback class to handle creation, upgrade and downgrade.
*/
@NonNull
public final SupportSQLiteOpenHelper.Callback callback;
- /**
- * The {@link DatabaseErrorHandler} to be used when sqlite reports database
- * corruption, or null to use the default error handler.
- */
- @Nullable
- public final DatabaseErrorHandler errorHandler;
- Configuration(@NonNull Context context, @Nullable String name,
- int version, @Nullable DatabaseErrorHandler errorHandler,
- @NonNull Callback callback) {
+ Configuration(@NonNull Context context, @Nullable String name, @NonNull Callback callback) {
this.context = context;
this.name = name;
- this.version = version;
this.callback = callback;
- this.errorHandler = errorHandler;
}
/**
@@ -255,9 +336,7 @@
public static class Builder {
Context mContext;
String mName;
- int mVersion = 1;
SupportSQLiteOpenHelper.Callback mCallback;
- DatabaseErrorHandler mErrorHandler;
public Configuration build() {
if (mCallback == null) {
@@ -268,11 +347,7 @@
throw new IllegalArgumentException("Must set a non-null context to create"
+ " the configuration.");
}
- if (mErrorHandler == null) {
- mErrorHandler = new DefaultDatabaseErrorHandler();
- }
- return new Configuration(mContext, mName, mVersion, mErrorHandler,
- mCallback);
+ return new Configuration(mContext, mName, mCallback);
}
Builder(@NonNull Context context) {
@@ -280,17 +355,6 @@
}
/**
- * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite
- * reports database corruption, or null to use the default error
- * handler.
- * @return This
- */
- public Builder errorHandler(@Nullable DatabaseErrorHandler errorHandler) {
- mErrorHandler = errorHandler;
- return this;
- }
-
- /**
* @param name Name of the database file, or null for an in-memory database.
* @return This
*/
@@ -307,20 +371,6 @@
mCallback = callback;
return this;
}
-
- /**
- * @param version Version number of the database (starting at 1); if the database is
- * older,
- * {@link SupportSQLiteOpenHelper.Callback#onUpgrade(SupportSQLiteDatabase, int, int)}
- * will be used to upgrade the database; if the database is newer,
- * {@link SupportSQLiteOpenHelper.Callback#onDowngrade(SupportSQLiteDatabase, int, int)}
- * will be used to downgrade the database.
- * @return this
- */
- public Builder version(int version) {
- mVersion = version;
- return this;
- }
}
}
diff --git a/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java b/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java
index 92a5820..e9c2b74 100644
--- a/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java
+++ b/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java
@@ -53,8 +53,7 @@
*
* @param delegate The delegate to receive all calls.
*/
- @SuppressWarnings("WeakerAccess")
- public FrameworkSQLiteDatabase(SQLiteDatabase delegate) {
+ FrameworkSQLiteDatabase(SQLiteDatabase delegate) {
mDelegate = delegate;
}
diff --git a/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelper.java b/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelper.java
index aa08fa4..a1690f4 100644
--- a/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelper.java
+++ b/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelper.java
@@ -28,42 +28,14 @@
class FrameworkSQLiteOpenHelper implements SupportSQLiteOpenHelper {
private final OpenHelper mDelegate;
- FrameworkSQLiteOpenHelper(Context context, String name, int version,
- DatabaseErrorHandler errorHandler,
- SupportSQLiteOpenHelper.Callback callback) {
- mDelegate = createDelegate(context, name, version, errorHandler, callback);
+ FrameworkSQLiteOpenHelper(Context context, String name,
+ Callback callback) {
+ mDelegate = createDelegate(context, name, callback);
}
- private OpenHelper createDelegate(Context context, String name,
- int version, DatabaseErrorHandler errorHandler,
- final Callback callback) {
- return new OpenHelper(context, name, null, version, errorHandler) {
- @Override
- public void onCreate(SQLiteDatabase sqLiteDatabase) {
- mWrappedDb = new FrameworkSQLiteDatabase(sqLiteDatabase);
- callback.onCreate(mWrappedDb);
- }
-
- @Override
- public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
- callback.onUpgrade(getWrappedDb(sqLiteDatabase), oldVersion, newVersion);
- }
-
- @Override
- public void onConfigure(SQLiteDatabase db) {
- callback.onConfigure(getWrappedDb(db));
- }
-
- @Override
- public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- callback.onDowngrade(getWrappedDb(db), oldVersion, newVersion);
- }
-
- @Override
- public void onOpen(SQLiteDatabase db) {
- callback.onOpen(getWrappedDb(db));
- }
- };
+ private OpenHelper createDelegate(Context context, String name, Callback callback) {
+ final FrameworkSQLiteDatabase[] dbRef = new FrameworkSQLiteDatabase[1];
+ return new OpenHelper(context, name, dbRef, callback);
}
@Override
@@ -92,14 +64,29 @@
mDelegate.close();
}
- abstract static class OpenHelper extends SQLiteOpenHelper {
+ static class OpenHelper extends SQLiteOpenHelper {
+ /**
+ * This is used as an Object reference so that we can access the wrapped database inside
+ * the constructor. SQLiteOpenHelper requires the error handler to be passed in the
+ * constructor.
+ */
+ final FrameworkSQLiteDatabase[] mDbRef;
+ final Callback mCallback;
- FrameworkSQLiteDatabase mWrappedDb;
-
- OpenHelper(Context context, String name,
- SQLiteDatabase.CursorFactory factory, int version,
- DatabaseErrorHandler errorHandler) {
- super(context, name, factory, version, errorHandler);
+ OpenHelper(Context context, String name, final FrameworkSQLiteDatabase[] dbRef,
+ final Callback callback) {
+ super(context, name, null, callback.version,
+ new DatabaseErrorHandler() {
+ @Override
+ public void onCorruption(SQLiteDatabase dbObj) {
+ FrameworkSQLiteDatabase db = dbRef[0];
+ if (db != null) {
+ callback.onCorruption(db);
+ }
+ }
+ });
+ mCallback = callback;
+ mDbRef = dbRef;
}
SupportSQLiteDatabase getWritableSupportDatabase() {
@@ -113,16 +100,43 @@
}
FrameworkSQLiteDatabase getWrappedDb(SQLiteDatabase sqLiteDatabase) {
- if (mWrappedDb == null) {
- mWrappedDb = new FrameworkSQLiteDatabase(sqLiteDatabase);
+ FrameworkSQLiteDatabase dbRef = mDbRef[0];
+ if (dbRef == null) {
+ dbRef = new FrameworkSQLiteDatabase(sqLiteDatabase);
+ mDbRef[0] = dbRef;
}
- return mWrappedDb;
+ return mDbRef[0];
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase sqLiteDatabase) {
+ mCallback.onCreate(getWrappedDb(sqLiteDatabase));
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
+ mCallback.onUpgrade(getWrappedDb(sqLiteDatabase), oldVersion, newVersion);
+ }
+
+ @Override
+ public void onConfigure(SQLiteDatabase db) {
+ mCallback.onConfigure(getWrappedDb(db));
+ }
+
+ @Override
+ public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ mCallback.onDowngrade(getWrappedDb(db), oldVersion, newVersion);
+ }
+
+ @Override
+ public void onOpen(SQLiteDatabase db) {
+ mCallback.onOpen(getWrappedDb(db));
}
@Override
public synchronized void close() {
super.close();
- mWrappedDb = null;
+ mDbRef[0] = null;
}
}
}
diff --git a/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelperFactory.java b/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelperFactory.java
index 2268f45..ab11d49 100644
--- a/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelperFactory.java
+++ b/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelperFactory.java
@@ -27,8 +27,6 @@
@Override
public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) {
return new FrameworkSQLiteOpenHelper(
- configuration.context, configuration.name,
- configuration.version, configuration.errorHandler, configuration.callback
- );
+ configuration.context, configuration.name, configuration.callback);
}
}
diff --git a/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java b/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java
index a2daf12..53a04bd 100644
--- a/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java
+++ b/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java
@@ -30,8 +30,7 @@
*
* @param delegate The SQLiteStatement to delegate calls to.
*/
- @SuppressWarnings("WeakerAccess")
- public FrameworkSQLiteStatement(SQLiteStatement delegate) {
+ FrameworkSQLiteStatement(SQLiteStatement delegate) {
mDelegate = delegate;
}
diff --git a/android/arch/persistence/room/Entity.java b/android/arch/persistence/room/Entity.java
index f54f0f8..94ca3bf 100644
--- a/android/arch/persistence/room/Entity.java
+++ b/android/arch/persistence/room/Entity.java
@@ -36,6 +36,9 @@
* When a class is marked as an Entity, all of its fields are persisted. If you would like to
* exclude some of its fields, you can mark them with {@link Ignore}.
* <p>
+ * If a field is {@code transient}, it is automatically ignored <b>unless</b> it is annotated with
+ * {@link ColumnInfo}, {@link Embedded} or {@link Relation}.
+ * <p>
* Example:
* <pre>
* {@literal @}Entity
diff --git a/android/arch/persistence/room/ForeignKey.java b/android/arch/persistence/room/ForeignKey.java
index 4ba0fb3..3ba632b 100644
--- a/android/arch/persistence/room/ForeignKey.java
+++ b/android/arch/persistence/room/ForeignKey.java
@@ -40,7 +40,7 @@
* <a href="https://sqlite.org/pragma.html#pragma_defer_foreign_keys">defer_foreign_keys</a> PRAGMA
* to defer them depending on your transaction.
* <p>
- * Please refer to the SQLite <a href="https://sqlite.org/foreignkeys.html>foreign keys</a>
+ * Please refer to the SQLite <a href="https://sqlite.org/foreignkeys.html">foreign keys</a>
* documentation for details.
*/
public @interface ForeignKey {
diff --git a/android/arch/persistence/room/InvalidationTracker.java b/android/arch/persistence/room/InvalidationTracker.java
index 33bc4ed..45ec028 100644
--- a/android/arch/persistence/room/InvalidationTracker.java
+++ b/android/arch/persistence/room/InvalidationTracker.java
@@ -16,7 +16,7 @@
package android.arch.persistence.room;
-import android.arch.core.executor.AppToolkitTaskExecutor;
+import android.arch.core.executor.ArchTaskExecutor;
import android.arch.core.internal.SafeIterableMap;
import android.arch.persistence.db.SupportSQLiteDatabase;
import android.arch.persistence.db.SupportSQLiteStatement;
@@ -166,10 +166,12 @@
private static void appendTriggerName(StringBuilder builder, String tableName,
String triggerType) {
- builder.append("room_table_modification_trigger_")
+ builder.append("`")
+ .append("room_table_modification_trigger_")
.append(tableName)
.append("_")
- .append(triggerType);
+ .append(triggerType)
+ .append("`");
}
private void stopTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
@@ -192,9 +194,9 @@
appendTriggerName(stringBuilder, tableName, trigger);
stringBuilder.append(" AFTER ")
.append(trigger)
- .append(" ON ")
+ .append(" ON `")
.append(tableName)
- .append(" BEGIN INSERT OR REPLACE INTO ")
+ .append("` BEGIN INSERT OR REPLACE INTO ")
.append(UPDATE_TABLE_NAME)
.append(" VALUES(null, ")
.append(tableId)
@@ -238,7 +240,7 @@
currentObserver = mObserverMap.putIfAbsent(observer, wrapper);
}
if (currentObserver == null && mObservedTableTracker.onAdded(tableIds)) {
- AppToolkitTaskExecutor.getInstance().executeOnDiskIO(mSyncTriggers);
+ ArchTaskExecutor.getInstance().executeOnDiskIO(mSyncTriggers);
}
}
@@ -269,7 +271,7 @@
wrapper = mObserverMap.remove(observer);
}
if (wrapper != null && mObservedTableTracker.onRemoved(wrapper.mTableIds)) {
- AppToolkitTaskExecutor.getInstance().executeOnDiskIO(mSyncTriggers);
+ ArchTaskExecutor.getInstance().executeOnDiskIO(mSyncTriggers);
}
}
@@ -350,11 +352,18 @@
return;
}
- if (mDatabase.inTransaction()
- || !mPendingRefresh.compareAndSet(true, false)) {
+ if (!mPendingRefresh.compareAndSet(true, false)) {
// no pending refresh
return;
}
+
+ if (mDatabase.inTransaction()) {
+ // current thread is in a transaction. when it ends, it will invoke
+ // refreshRunnable again. mPendingRefresh is left as false on purpose
+ // so that the last transaction can flip it on again.
+ return;
+ }
+
mCleanupStatement.executeUpdateDelete();
mQueryArgs[0] = mMaxVersion;
Cursor cursor = mDatabase.query(SELECT_UPDATED_TABLES_SQL, mQueryArgs);
@@ -400,7 +409,7 @@
public void refreshVersionsAsync() {
// TODO we should consider doing this sync instead of async.
if (mPendingRefresh.compareAndSet(false, true)) {
- AppToolkitTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable);
+ ArchTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable);
}
}
diff --git a/android/arch/persistence/room/InvalidationTrackerTest.java b/android/arch/persistence/room/InvalidationTrackerTest.java
index f0b730a..d7474fd 100644
--- a/android/arch/persistence/room/InvalidationTrackerTest.java
+++ b/android/arch/persistence/room/InvalidationTrackerTest.java
@@ -247,7 +247,7 @@
mTracker.mRefreshRunnable.run();
}
- @Test
+ // @Test - disabled due to flakiness b/65257997
public void closedDbAfterOpen() throws InterruptedException {
setVersions(3, 1);
mTracker.addObserver(new LatchObserver(1, "a", "b"));
diff --git a/android/arch/persistence/room/RoomDatabase.java b/android/arch/persistence/room/RoomDatabase.java
index e64f2d6..cdad868 100644
--- a/android/arch/persistence/room/RoomDatabase.java
+++ b/android/arch/persistence/room/RoomDatabase.java
@@ -16,7 +16,7 @@
package android.arch.persistence.room;
-import android.arch.core.executor.AppToolkitTaskExecutor;
+import android.arch.core.executor.ArchTaskExecutor;
import android.arch.persistence.db.SimpleSQLiteQuery;
import android.arch.persistence.db.SupportSQLiteDatabase;
import android.arch.persistence.db.SupportSQLiteOpenHelper;
@@ -158,7 +158,7 @@
if (mAllowMainThreadQueries) {
return;
}
- if (AppToolkitTaskExecutor.getInstance().isMainThread()) {
+ if (ArchTaskExecutor.getInstance().isMainThread()) {
throw new IllegalStateException("Cannot access database on the main thread since"
+ " it may potentially lock the UI for a long period of time.");
}
@@ -216,7 +216,11 @@
*/
public void endTransaction() {
mOpenHelper.getWritableDatabase().endTransaction();
- mInvalidationTracker.refreshVersionsAsync();
+ if (!inTransaction()) {
+ // enqueue refresh only if we are NOT in a transaction. Otherwise, wait for the last
+ // endTransaction call to do it.
+ mInvalidationTracker.refreshVersionsAsync();
+ }
}
/**
@@ -311,7 +315,6 @@
private ArrayList<Callback> mCallbacks;
private SupportSQLiteOpenHelper.Factory mFactory;
- private boolean mInMemory;
private boolean mAllowMainThreadQueries;
private boolean mRequireMigration;
/**
@@ -381,6 +384,9 @@
}
/**
+ * Allows Room to destructively recreate database tables if {@link Migration}s that would
+ * migrate old database schemas to the latest schema version are not found.
+ * <p>
* When the database version on the device does not match the latest schema version, Room
* runs necessary {@link Migration}s on the database.
* <p>
diff --git a/android/arch/persistence/room/RoomOpenHelper.java b/android/arch/persistence/room/RoomOpenHelper.java
index 8767f06..47279d6 100644
--- a/android/arch/persistence/room/RoomOpenHelper.java
+++ b/android/arch/persistence/room/RoomOpenHelper.java
@@ -44,6 +44,7 @@
public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
@NonNull String identityHash) {
+ super(delegate.version);
mConfiguration = configuration;
mDelegate = delegate;
mIdentityHash = identityHash;
@@ -135,6 +136,12 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public abstract static class Delegate {
+ public final int version;
+
+ public Delegate(int version) {
+ this.version = version;
+ }
+
protected abstract void dropAllTables(SupportSQLiteDatabase database);
protected abstract void createAllTables(SupportSQLiteDatabase database);
diff --git a/android/arch/persistence/room/RoomWarnings.java b/android/arch/persistence/room/RoomWarnings.java
index 91f32e4..c64be96 100644
--- a/android/arch/persistence/room/RoomWarnings.java
+++ b/android/arch/persistence/room/RoomWarnings.java
@@ -117,4 +117,12 @@
*/
public static final String MISSING_INDEX_ON_FOREIGN_KEY_CHILD =
"ROOM_MISSING_FOREIGN_KEY_CHILD_INDEX";
+
+ /**
+ * Reported when a Pojo has multiple constructors, one of which is a no-arg constructor. Room
+ * will pick that one by default but will print this warning in case the constructor choice is
+ * important. You can always guide Room to use the right constructor using the @Ignore
+ * annotation.
+ */
+ public static final String DEFAULT_CONSTRUCTOR = "ROOM_DEFAULT_CONSTRUCTOR";
}
diff --git a/android/arch/persistence/room/RxRoom.java b/android/arch/persistence/room/RxRoom.java
index adfca27..285b3f8 100644
--- a/android/arch/persistence/room/RxRoom.java
+++ b/android/arch/persistence/room/RxRoom.java
@@ -16,7 +16,7 @@
package android.arch.persistence.room;
-import android.arch.core.executor.AppToolkitTaskExecutor;
+import android.arch.core.executor.ArchTaskExecutor;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
@@ -133,7 +133,7 @@
public Disposable schedule(@NonNull Runnable run, long delay,
@NonNull TimeUnit unit) {
DisposableRunnable disposable = new DisposableRunnable(run, mDisposed);
- AppToolkitTaskExecutor.getInstance().executeOnDiskIO(run);
+ ArchTaskExecutor.getInstance().executeOnDiskIO(run);
return disposable;
}
diff --git a/android/arch/persistence/room/Transaction.java b/android/arch/persistence/room/Transaction.java
new file mode 100644
index 0000000..914e4f4
--- /dev/null
+++ b/android/arch/persistence/room/Transaction.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a method in an abstract {@link Dao} class as a transaction method.
+ * <p>
+ * The derived implementation of the method will execute the super method in a database transaction.
+ * All the parameters and return types are preserved. The transaction will be marked as successful
+ * unless an exception is thrown in the method body.
+ * <p>
+ * Example:
+ * <pre>
+ * {@literal @}Dao
+ * public abstract class ProductDao {
+ * {@literal @}Insert
+ * public abstract void insert(Product product);
+ * {@literal @}Delete
+ * public abstract void delete(Product product);
+ * {@literal @}Transaction
+ * public void insertAndDeleteInTransaction(Product newProduct, Product oldProduct) {
+ * // Anything inside this method runs in a single transaction.
+ * insert(newProduct);
+ * delete(oldProduct);
+ * }
+ * }
+ * </pre>
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.CLASS)
+public @interface Transaction {
+}
diff --git a/android/arch/persistence/room/integration/testapp/CustomerViewModel.java b/android/arch/persistence/room/integration/testapp/CustomerViewModel.java
index 1f434ad..320b2cd 100644
--- a/android/arch/persistence/room/integration/testapp/CustomerViewModel.java
+++ b/android/arch/persistence/room/integration/testapp/CustomerViewModel.java
@@ -17,7 +17,7 @@
package android.arch.persistence.room.integration.testapp;
import android.app.Application;
-import android.arch.core.executor.AppToolkitTaskExecutor;
+import android.arch.core.executor.ArchTaskExecutor;
import android.arch.lifecycle.AndroidViewModel;
import android.arch.lifecycle.LiveData;
import android.arch.paging.DataSource;
@@ -47,7 +47,7 @@
mDatabase = Room.databaseBuilder(this.getApplication(),
SampleDatabase.class, "customerDatabase").build();
- AppToolkitTaskExecutor.getInstance().executeOnDiskIO(new Runnable() {
+ ArchTaskExecutor.getInstance().executeOnDiskIO(new Runnable() {
@Override
public void run() {
// fill with some simple data
@@ -73,7 +73,7 @@
}
void insertCustomer() {
- AppToolkitTaskExecutor.getInstance().executeOnDiskIO(new Runnable() {
+ ArchTaskExecutor.getInstance().executeOnDiskIO(new Runnable() {
@Override
public void run() {
mDatabase.getCustomerDao().insert(createCustomer());
diff --git a/android/arch/persistence/room/integration/testapp/PKeyTestDatabase.java b/android/arch/persistence/room/integration/testapp/PKeyTestDatabase.java
index e61d808..63b9507 100644
--- a/android/arch/persistence/room/integration/testapp/PKeyTestDatabase.java
+++ b/android/arch/persistence/room/integration/testapp/PKeyTestDatabase.java
@@ -23,13 +23,18 @@
import android.arch.persistence.room.RoomDatabase;
import android.arch.persistence.room.integration.testapp.vo.IntAutoIncPKeyEntity;
import android.arch.persistence.room.integration.testapp.vo.IntegerAutoIncPKeyEntity;
+import android.arch.persistence.room.integration.testapp.vo.IntegerPKeyEntity;
+import android.arch.persistence.room.integration.testapp.vo.ObjectPKeyEntity;
import java.util.List;
-@Database(entities = {IntAutoIncPKeyEntity.class, IntegerAutoIncPKeyEntity.class}, version = 1,
+@Database(entities = {IntAutoIncPKeyEntity.class, IntegerAutoIncPKeyEntity.class,
+ ObjectPKeyEntity.class, IntegerPKeyEntity.class}, version = 1,
exportSchema = false)
public abstract class PKeyTestDatabase extends RoomDatabase {
public abstract IntPKeyDao intPKeyDao();
+ public abstract IntegerAutoIncPKeyDao integerAutoIncPKeyDao();
+ public abstract ObjectPKeyDao objectPKeyDao();
public abstract IntegerPKeyDao integerPKeyDao();
@Dao
@@ -50,9 +55,10 @@
}
@Dao
- public interface IntegerPKeyDao {
+ public interface IntegerAutoIncPKeyDao {
@Insert
- void insertMe(IntegerAutoIncPKeyEntity items);
+ void insertMe(IntegerAutoIncPKeyEntity item);
+
@Query("select * from IntegerAutoIncPKeyEntity WHERE pKey = :key")
IntegerAutoIncPKeyEntity getMe(int key);
@@ -65,4 +71,19 @@
@Query("select data from IntegerAutoIncPKeyEntity WHERE pKey IN(:ids)")
List<String> loadDataById(long... ids);
}
+
+ @Dao
+ public interface ObjectPKeyDao {
+ @Insert
+ void insertMe(ObjectPKeyEntity item);
+ }
+
+ @Dao
+ public interface IntegerPKeyDao {
+ @Insert
+ void insertMe(IntegerPKeyEntity item);
+
+ @Query("select * from IntegerPKeyEntity")
+ List<IntegerPKeyEntity> loadAll();
+ }
}
diff --git a/android/arch/persistence/room/integration/testapp/TestDatabase.java b/android/arch/persistence/room/integration/testapp/TestDatabase.java
index 9417296..2fad7b1 100644
--- a/android/arch/persistence/room/integration/testapp/TestDatabase.java
+++ b/android/arch/persistence/room/integration/testapp/TestDatabase.java
@@ -21,6 +21,7 @@
import android.arch.persistence.room.TypeConverter;
import android.arch.persistence.room.TypeConverters;
import android.arch.persistence.room.integration.testapp.dao.BlobEntityDao;
+import android.arch.persistence.room.integration.testapp.dao.FunnyNamedDao;
import android.arch.persistence.room.integration.testapp.dao.PetCoupleDao;
import android.arch.persistence.room.integration.testapp.dao.PetDao;
import android.arch.persistence.room.integration.testapp.dao.ProductDao;
@@ -31,6 +32,7 @@
import android.arch.persistence.room.integration.testapp.dao.UserPetDao;
import android.arch.persistence.room.integration.testapp.dao.WithClauseDao;
import android.arch.persistence.room.integration.testapp.vo.BlobEntity;
+import android.arch.persistence.room.integration.testapp.vo.FunnyNamedEntity;
import android.arch.persistence.room.integration.testapp.vo.Pet;
import android.arch.persistence.room.integration.testapp.vo.PetCouple;
import android.arch.persistence.room.integration.testapp.vo.Product;
@@ -41,7 +43,7 @@
import java.util.Date;
@Database(entities = {User.class, Pet.class, School.class, PetCouple.class, Toy.class,
- BlobEntity.class, Product.class},
+ BlobEntity.class, Product.class, FunnyNamedEntity.class},
version = 1, exportSchema = false)
@TypeConverters(TestDatabase.Converters.class)
public abstract class TestDatabase extends RoomDatabase {
@@ -55,6 +57,7 @@
public abstract ProductDao getProductDao();
public abstract SpecificDogDao getSpecificDogDao();
public abstract WithClauseDao getWithClauseDao();
+ public abstract FunnyNamedDao getFunnyNamedDao();
@SuppressWarnings("unused")
public static class Converters {
diff --git a/android/arch/persistence/room/integration/testapp/dao/FunnyNamedDao.java b/android/arch/persistence/room/integration/testapp/dao/FunnyNamedDao.java
new file mode 100644
index 0000000..93b5e72
--- /dev/null
+++ b/android/arch/persistence/room/integration/testapp/dao/FunnyNamedDao.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.integration.testapp.dao;
+
+import static android.arch.persistence.room.integration.testapp.vo.FunnyNamedEntity.COLUMN_ID;
+import static android.arch.persistence.room.integration.testapp.vo.FunnyNamedEntity.TABLE_NAME;
+
+import android.arch.lifecycle.LiveData;
+import android.arch.persistence.room.Dao;
+import android.arch.persistence.room.Delete;
+import android.arch.persistence.room.Insert;
+import android.arch.persistence.room.Query;
+import android.arch.persistence.room.Update;
+import android.arch.persistence.room.integration.testapp.vo.FunnyNamedEntity;
+
+import java.util.List;
+
+@Dao
+public interface FunnyNamedDao {
+ String SELECT_ONE = "select * from \"" + TABLE_NAME + "\" WHERE \"" + COLUMN_ID + "\" = :id";
+ @Insert
+ void insert(FunnyNamedEntity... entities);
+ @Delete
+ void delete(FunnyNamedEntity... entities);
+ @Update
+ void update(FunnyNamedEntity... entities);
+
+ @Query("select * from \"" + TABLE_NAME + "\" WHERE \"" + COLUMN_ID + "\" IN (:ids)")
+ List<FunnyNamedEntity> loadAll(int... ids);
+
+ @Query(SELECT_ONE)
+ LiveData<FunnyNamedEntity> observableOne(int id);
+
+ @Query(SELECT_ONE)
+ FunnyNamedEntity load(int id);
+}
diff --git a/android/arch/persistence/room/integration/testapp/dao/SchoolDao.java b/android/arch/persistence/room/integration/testapp/dao/SchoolDao.java
index 7bb137f..18e8d93 100644
--- a/android/arch/persistence/room/integration/testapp/dao/SchoolDao.java
+++ b/android/arch/persistence/room/integration/testapp/dao/SchoolDao.java
@@ -35,16 +35,16 @@
@Query("SELECT * from School WHERE address_street LIKE '%' || :street || '%'")
public abstract List<School> findByStreet(String street);
- @Query("SELECT mName, manager_mName FROM School")
+ @Query("SELECT mId, mName, manager_mName FROM School")
public abstract List<School> schoolAndManagerNames();
- @Query("SELECT mName, manager_mName FROM School")
+ @Query("SELECT mId, mName, manager_mName FROM School")
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
public abstract List<SchoolRef> schoolAndManagerNamesAsPojo();
@Query("SELECT address_lat as lat, address_lng as lng FROM School WHERE mId = :schoolId")
public abstract Coordinates loadCoordinates(int schoolId);
- @Query("SELECT address_lat, address_lng FROM School WHERE mId = :schoolId")
+ @Query("SELECT mId, address_lat, address_lng FROM School WHERE mId = :schoolId")
public abstract School loadCoordinatesAsSchool(int schoolId);
}
diff --git a/android/arch/persistence/room/integration/testapp/dao/UserDao.java b/android/arch/persistence/room/integration/testapp/dao/UserDao.java
index 337c233..665a1ae 100644
--- a/android/arch/persistence/room/integration/testapp/dao/UserDao.java
+++ b/android/arch/persistence/room/integration/testapp/dao/UserDao.java
@@ -24,6 +24,7 @@
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.OnConflictStrategy;
import android.arch.persistence.room.Query;
+import android.arch.persistence.room.Transaction;
import android.arch.persistence.room.Update;
import android.arch.persistence.room.integration.testapp.TestDatabase;
import android.arch.persistence.room.integration.testapp.vo.AvgWeightByAge;
@@ -259,4 +260,10 @@
+ " WHERE mLastName > :lastName or (mLastName = :lastName and (mName < :name or (mName = :name and mId > :id)))"
+ " ORDER BY mLastName ASC, mName DESC, mId ASC")
public abstract int userComplexCountBefore(String lastName, String name, int id);
+
+ @Transaction
+ public void insertBothByAnnotation(final User a, final User b) {
+ insert(a);
+ insert(b);
+ }
}
diff --git a/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java b/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java
index 3507aee..eb15901 100644
--- a/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java
+++ b/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java
@@ -33,6 +33,8 @@
import java.util.List;
+import io.reactivex.Flowable;
+
@Dao
public interface UserPetDao {
@Query("SELECT * FROM User u, Pet p WHERE u.mId = p.mUserId")
@@ -62,6 +64,9 @@
@Query("SELECT * FROM User u where u.mId = :userId")
LiveData<UserAndAllPets> liveUserWithPets(int userId);
+ @Query("SELECT * FROM User u where u.mId = :userId")
+ Flowable<UserAndAllPets> flowableUserWithPets(int userId);
+
@Query("SELECT * FROM User u where u.mId = :uid")
EmbeddedUserAndAllPets loadUserAndPetsAsEmbedded(int uid);
diff --git a/android/arch/persistence/room/integration/testapp/dao/WithClauseDao.java b/android/arch/persistence/room/integration/testapp/dao/WithClauseDao.java
index b1c38ed..40098ed 100644
--- a/android/arch/persistence/room/integration/testapp/dao/WithClauseDao.java
+++ b/android/arch/persistence/room/integration/testapp/dao/WithClauseDao.java
@@ -16,13 +16,16 @@
package android.arch.persistence.room.integration.testapp.dao;
+import android.annotation.TargetApi;
import android.arch.lifecycle.LiveData;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Query;
+import android.os.Build;
import java.util.List;
@Dao
+@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public interface WithClauseDao {
@Query("WITH RECURSIVE factorial(n, fact) AS \n"
+ "(SELECT 0, 1 \n"
diff --git a/android/arch/persistence/room/integration/testapp/database/SampleDatabase.java b/android/arch/persistence/room/integration/testapp/database/SampleDatabase.java
index eec59f6..9020eb1 100644
--- a/android/arch/persistence/room/integration/testapp/database/SampleDatabase.java
+++ b/android/arch/persistence/room/integration/testapp/database/SampleDatabase.java
@@ -18,10 +18,6 @@
import android.arch.persistence.room.Database;
import android.arch.persistence.room.RoomDatabase;
-import android.arch.persistence.room.TypeConverter;
-import android.arch.persistence.room.TypeConverters;
-
-import java.util.Date;
/**
* Sample database of customers.
diff --git a/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java b/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
index 725d53f..7fe2bc9 100644
--- a/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
+++ b/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
@@ -318,6 +318,8 @@
+ " (`id` INTEGER NOT NULL, `name` TEXT COLLATE NOCASE, PRIMARY KEY(`id`),"
+ " FOREIGN KEY(`name`) REFERENCES `Entity1`(`name`)"
+ " ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED)");
+ database.execSQL("CREATE UNIQUE INDEX `index_entity1` ON "
+ + MigrationDb.Entity1.TABLE_NAME + " (`name`)");
}
};
diff --git a/android/arch/persistence/room/integration/testapp/paging/LivePagedListProviderTest.java b/android/arch/persistence/room/integration/testapp/paging/LivePagedListProviderTest.java
index 4c9d73e..df70a17 100644
--- a/android/arch/persistence/room/integration/testapp/paging/LivePagedListProviderTest.java
+++ b/android/arch/persistence/room/integration/testapp/paging/LivePagedListProviderTest.java
@@ -21,17 +21,17 @@
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
-import android.arch.core.executor.AppToolkitTaskExecutor;
+import android.arch.core.executor.ArchTaskExecutor;
import android.arch.core.executor.testing.CountingTaskExecutorRule;
import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.LifecycleOwner;
import android.arch.lifecycle.LifecycleRegistry;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.Observer;
+import android.arch.paging.PagedList;
import android.arch.persistence.room.integration.testapp.test.TestDatabaseTest;
import android.arch.persistence.room.integration.testapp.test.TestUtil;
import android.arch.persistence.room.integration.testapp.vo.User;
-import android.arch.paging.PagedList;
import android.support.annotation.Nullable;
import android.support.test.filters.LargeTest;
import android.support.test.runner.AndroidJUnit4;
@@ -131,7 +131,7 @@
return null;
}
});
- AppToolkitTaskExecutor.getInstance().executeOnMainThread(futureTask);
+ ArchTaskExecutor.getInstance().executeOnMainThread(futureTask);
futureTask.get();
}
@@ -155,7 +155,7 @@
private static class PagedListObserver<T> implements Observer<PagedList<T>> {
private PagedList<T> mList;
- public void reset() {
+ void reset() {
mList = null;
}
diff --git a/android/arch/persistence/room/integration/testapp/test/CustomDatabaseTest.java b/android/arch/persistence/room/integration/testapp/test/CustomDatabaseTest.java
index 353c2e3..6f44546 100644
--- a/android/arch/persistence/room/integration/testapp/test/CustomDatabaseTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/CustomDatabaseTest.java
@@ -55,7 +55,6 @@
Customer customer = new Customer();
for (int i = 0; i < 100; i++) {
SampleDatabase db = builder.build();
- customer.setId(i);
db.getCustomerDao().insert(customer);
// Give InvalidationTracker enough time to start #mRefreshRunnable and pass the
// initialization check.
diff --git a/android/arch/persistence/room/integration/testapp/test/FunnyNamedDaoTest.java b/android/arch/persistence/room/integration/testapp/test/FunnyNamedDaoTest.java
new file mode 100644
index 0000000..f4fca7f
--- /dev/null
+++ b/android/arch/persistence/room/integration/testapp/test/FunnyNamedDaoTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.arch.core.executor.testing.CountingTaskExecutorRule;
+import android.arch.lifecycle.Observer;
+import android.arch.persistence.room.integration.testapp.vo.FunnyNamedEntity;
+import android.support.annotation.Nullable;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class FunnyNamedDaoTest extends TestDatabaseTest {
+ @Rule
+ public CountingTaskExecutorRule mExecutorRule = new CountingTaskExecutorRule();
+
+ @Test
+ public void readWrite() {
+ FunnyNamedEntity entity = new FunnyNamedEntity(1, "a");
+ mFunnyNamedDao.insert(entity);
+ FunnyNamedEntity loaded = mFunnyNamedDao.load(1);
+ assertThat(loaded, is(entity));
+ }
+
+ @Test
+ public void update() {
+ FunnyNamedEntity entity = new FunnyNamedEntity(1, "a");
+ mFunnyNamedDao.insert(entity);
+ entity.setValue("b");
+ mFunnyNamedDao.update(entity);
+ FunnyNamedEntity loaded = mFunnyNamedDao.load(1);
+ assertThat(loaded.getValue(), is("b"));
+ }
+
+ @Test
+ public void delete() {
+ FunnyNamedEntity entity = new FunnyNamedEntity(1, "a");
+ mFunnyNamedDao.insert(entity);
+ assertThat(mFunnyNamedDao.load(1), notNullValue());
+ mFunnyNamedDao.delete(entity);
+ assertThat(mFunnyNamedDao.load(1), nullValue());
+ }
+
+ @Test
+ public void observe() throws TimeoutException, InterruptedException {
+ final FunnyNamedEntity[] item = new FunnyNamedEntity[1];
+ mFunnyNamedDao.observableOne(2).observeForever(new Observer<FunnyNamedEntity>() {
+ @Override
+ public void onChanged(@Nullable FunnyNamedEntity funnyNamedEntity) {
+ item[0] = funnyNamedEntity;
+ }
+ });
+
+ FunnyNamedEntity entity = new FunnyNamedEntity(1, "a");
+ mFunnyNamedDao.insert(entity);
+ mExecutorRule.drainTasks(1, TimeUnit.MINUTES);
+ assertThat(item[0], nullValue());
+
+ final FunnyNamedEntity entity2 = new FunnyNamedEntity(2, "b");
+ mFunnyNamedDao.insert(entity2);
+ mExecutorRule.drainTasks(1, TimeUnit.MINUTES);
+ assertThat(item[0], is(entity2));
+
+ final FunnyNamedEntity entity3 = new FunnyNamedEntity(2, "c");
+ mFunnyNamedDao.update(entity3);
+ mExecutorRule.drainTasks(1, TimeUnit.MINUTES);
+ assertThat(item[0], is(entity3));
+ }
+}
diff --git a/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java b/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java
index 4787ce5..84f20ec 100644
--- a/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java
@@ -21,7 +21,7 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
-import android.arch.core.executor.AppToolkitTaskExecutor;
+import android.arch.core.executor.ArchTaskExecutor;
import android.arch.core.executor.TaskExecutor;
import android.arch.persistence.room.InvalidationTracker;
import android.arch.persistence.room.Room;
@@ -68,7 +68,7 @@
@Before
public void setSingleThreadedIO() {
- AppToolkitTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
+ ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
ExecutorService mIOExecutor = Executors.newSingleThreadExecutor();
Handler mHandler = new Handler(Looper.getMainLooper());
@@ -91,7 +91,7 @@
@After
public void clearExecutor() {
- AppToolkitTaskExecutor.getInstance().setDelegate(null);
+ ArchTaskExecutor.getInstance().setDelegate(null);
}
private void waitUntilIOThreadIsIdle() {
@@ -101,7 +101,7 @@
return null;
}
});
- AppToolkitTaskExecutor.getInstance().executeOnDiskIO(future);
+ ArchTaskExecutor.getInstance().executeOnDiskIO(future);
//noinspection TryWithIdenticalCatches
try {
future.get();
diff --git a/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java b/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java
index cae8445..d78411f 100644
--- a/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java
@@ -21,7 +21,7 @@
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
-import android.arch.core.executor.AppToolkitTaskExecutor;
+import android.arch.core.executor.ArchTaskExecutor;
import android.arch.core.executor.testing.CountingTaskExecutorRule;
import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.LifecycleOwner;
@@ -35,9 +35,11 @@
import android.arch.persistence.room.integration.testapp.vo.Toy;
import android.arch.persistence.room.integration.testapp.vo.User;
import android.arch.persistence.room.integration.testapp.vo.UserAndAllPets;
+import android.os.Build;
import android.support.annotation.Nullable;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest;
+import android.support.test.filters.SdkSuppress;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
@@ -235,6 +237,7 @@
}
@Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
public void withWithClause() throws ExecutionException, InterruptedException,
TimeoutException {
LiveData<List<String>> actual =
@@ -322,7 +325,7 @@
return null;
}
});
- AppToolkitTaskExecutor.getInstance().executeOnMainThread(futureTask);
+ ArchTaskExecutor.getInstance().executeOnMainThread(futureTask);
futureTask.get();
}
diff --git a/android/arch/persistence/room/integration/testapp/test/PrimaryKeyTest.java b/android/arch/persistence/room/integration/testapp/test/PrimaryKeyTest.java
index 97ce10c..fda4373 100644
--- a/android/arch/persistence/room/integration/testapp/test/PrimaryKeyTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/PrimaryKeyTest.java
@@ -16,29 +16,35 @@
package android.arch.persistence.room.integration.testapp.test;
+import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
-
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import static org.junit.Assert.assertNotNull;
import android.arch.persistence.room.Room;
import android.arch.persistence.room.integration.testapp.PKeyTestDatabase;
import android.arch.persistence.room.integration.testapp.vo.IntAutoIncPKeyEntity;
import android.arch.persistence.room.integration.testapp.vo.IntegerAutoIncPKeyEntity;
+import android.arch.persistence.room.integration.testapp.vo.IntegerPKeyEntity;
+import android.arch.persistence.room.integration.testapp.vo.ObjectPKeyEntity;
+import android.database.sqlite.SQLiteConstraintException;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Arrays;
+import java.util.List;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class PrimaryKeyTest {
private PKeyTestDatabase mDatabase;
+
@Before
public void setup() {
mDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(),
@@ -49,8 +55,8 @@
public void integerTest() {
IntegerAutoIncPKeyEntity entity = new IntegerAutoIncPKeyEntity();
entity.data = "foo";
- mDatabase.integerPKeyDao().insertMe(entity);
- IntegerAutoIncPKeyEntity loaded = mDatabase.integerPKeyDao().getMe(1);
+ mDatabase.integerAutoIncPKeyDao().insertMe(entity);
+ IntegerAutoIncPKeyEntity loaded = mDatabase.integerAutoIncPKeyDao().getMe(1);
assertThat(loaded, notNullValue());
assertThat(loaded.data, is(entity.data));
}
@@ -60,8 +66,8 @@
IntegerAutoIncPKeyEntity entity = new IntegerAutoIncPKeyEntity();
entity.pKey = 0;
entity.data = "foo";
- mDatabase.integerPKeyDao().insertMe(entity);
- IntegerAutoIncPKeyEntity loaded = mDatabase.integerPKeyDao().getMe(0);
+ mDatabase.integerAutoIncPKeyDao().insertMe(entity);
+ IntegerAutoIncPKeyEntity loaded = mDatabase.integerAutoIncPKeyDao().getMe(0);
assertThat(loaded, notNullValue());
assertThat(loaded.data, is(entity.data));
}
@@ -98,8 +104,8 @@
public void getInsertedIdFromInteger() {
IntegerAutoIncPKeyEntity entity = new IntegerAutoIncPKeyEntity();
entity.data = "foo";
- final long id = mDatabase.integerPKeyDao().insertAndGetId(entity);
- assertThat(mDatabase.integerPKeyDao().getMe((int) id).data, is("foo"));
+ final long id = mDatabase.integerAutoIncPKeyDao().insertAndGetId(entity);
+ assertThat(mDatabase.integerAutoIncPKeyDao().getMe((int) id).data, is("foo"));
}
@Test
@@ -108,7 +114,34 @@
entity.data = "foo";
IntegerAutoIncPKeyEntity entity2 = new IntegerAutoIncPKeyEntity();
entity2.data = "foo2";
- final long[] ids = mDatabase.integerPKeyDao().insertAndGetIds(entity, entity2);
- assertThat(mDatabase.integerPKeyDao().loadDataById(ids), is(Arrays.asList("foo", "foo2")));
+ final long[] ids = mDatabase.integerAutoIncPKeyDao().insertAndGetIds(entity, entity2);
+ assertThat(mDatabase.integerAutoIncPKeyDao().loadDataById(ids),
+ is(Arrays.asList("foo", "foo2")));
+ }
+
+ @Test
+ public void insertNullPrimaryKey() throws Exception {
+ ObjectPKeyEntity o1 = new ObjectPKeyEntity(null, "1");
+
+ Throwable throwable = null;
+ try {
+ mDatabase.objectPKeyDao().insertMe(o1);
+ } catch (Throwable t) {
+ throwable = t;
+ }
+ assertNotNull("Was expecting an exception", throwable);
+ assertThat(throwable, instanceOf(SQLiteConstraintException.class));
+ }
+
+ @Test
+ public void insertNullPrimaryKeyForInteger() throws Exception {
+ IntegerPKeyEntity entity = new IntegerPKeyEntity();
+ entity.data = "data";
+ mDatabase.integerPKeyDao().insertMe(entity);
+
+ List<IntegerPKeyEntity> list = mDatabase.integerPKeyDao().loadAll();
+ assertThat(list.size(), is(1));
+ assertThat(list.get(0).data, is("data"));
+ assertNotNull(list.get(0).pKey);
}
}
diff --git a/android/arch/persistence/room/integration/testapp/test/RxJava2Test.java b/android/arch/persistence/room/integration/testapp/test/RxJava2Test.java
index 1bbc140..01d071e 100644
--- a/android/arch/persistence/room/integration/testapp/test/RxJava2Test.java
+++ b/android/arch/persistence/room/integration/testapp/test/RxJava2Test.java
@@ -19,10 +19,12 @@
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
-import android.arch.core.executor.AppToolkitTaskExecutor;
+import android.arch.core.executor.ArchTaskExecutor;
import android.arch.core.executor.TaskExecutor;
import android.arch.persistence.room.EmptyResultSetException;
+import android.arch.persistence.room.integration.testapp.vo.Pet;
import android.arch.persistence.room.integration.testapp.vo.User;
+import android.arch.persistence.room.integration.testapp.vo.UserAndAllPets;
import android.support.test.filters.MediumTest;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
@@ -38,6 +40,7 @@
import java.util.List;
import io.reactivex.disposables.Disposable;
+import io.reactivex.functions.Predicate;
import io.reactivex.observers.TestObserver;
import io.reactivex.schedulers.TestScheduler;
import io.reactivex.subscribers.TestSubscriber;
@@ -52,7 +55,7 @@
public void setupSchedulers() {
mTestScheduler = new TestScheduler();
mTestScheduler.start();
- AppToolkitTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
+ ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
@Override
public void executeOnDiskIO(Runnable runnable) {
mTestScheduler.scheduleDirect(runnable);
@@ -73,7 +76,7 @@
@After
public void clearSchedulers() {
mTestScheduler.shutdown();
- AppToolkitTaskExecutor.getInstance().setDelegate(null);
+ ArchTaskExecutor.getInstance().setDelegate(null);
}
private void drain() throws InterruptedException {
@@ -269,4 +272,60 @@
subscriber.cancel();
subscriber.assertNoErrors();
}
+
+ @Test
+ public void flowableWithRelation() throws InterruptedException {
+ final TestSubscriber<UserAndAllPets> subscriber = new TestSubscriber<>();
+
+ mUserPetDao.flowableUserWithPets(3).subscribe(subscriber);
+ drain();
+ subscriber.assertSubscribed();
+
+ drain();
+ subscriber.assertNoValues();
+
+ final User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ drain();
+ subscriber.assertValue(new Predicate<UserAndAllPets>() {
+ @Override
+ public boolean test(UserAndAllPets userAndAllPets) throws Exception {
+ return userAndAllPets.user.equals(user);
+ }
+ });
+ subscriber.assertValueCount(1);
+ final Pet[] pets = TestUtil.createPetsForUser(3, 1, 2);
+ mPetDao.insertAll(pets);
+ drain();
+ subscriber.assertValueAt(1, new Predicate<UserAndAllPets>() {
+ @Override
+ public boolean test(UserAndAllPets userAndAllPets) throws Exception {
+ return userAndAllPets.user.equals(user)
+ && userAndAllPets.pets.equals(Arrays.asList(pets));
+ }
+ });
+ }
+
+ @Test
+ public void flowable_updateInTransaction() throws InterruptedException {
+ // When subscribing to the emissions of the user
+ final TestSubscriber<User> userTestSubscriber = mUserDao
+ .flowableUserById(3)
+ .observeOn(mTestScheduler)
+ .test();
+ drain();
+ userTestSubscriber.assertValueCount(0);
+
+ // When inserting a new user in the data source
+ mDatabase.beginTransaction();
+ try {
+ mUserDao.insert(TestUtil.createUser(3));
+ mDatabase.setTransactionSuccessful();
+
+ } finally {
+ mDatabase.endTransaction();
+ }
+ drain();
+ userTestSubscriber.assertValueCount(1);
+ }
}
diff --git a/android/arch/persistence/room/integration/testapp/test/RxJava2WithInstantTaskExecutorTest.java b/android/arch/persistence/room/integration/testapp/test/RxJava2WithInstantTaskExecutorTest.java
new file mode 100644
index 0000000..fcd0b00
--- /dev/null
+++ b/android/arch/persistence/room/integration/testapp/test/RxJava2WithInstantTaskExecutorTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.integration.testapp.test;
+
+import android.arch.core.executor.testing.InstantTaskExecutorRule;
+import android.arch.persistence.room.Room;
+import android.arch.persistence.room.integration.testapp.TestDatabase;
+import android.arch.persistence.room.integration.testapp.vo.User;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import io.reactivex.subscribers.TestSubscriber;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RxJava2WithInstantTaskExecutorTest {
+ @Rule
+ public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
+
+ private TestDatabase mDatabase;
+
+ @Before
+ public void initDb() throws Exception {
+ // using an in-memory database because the information stored here disappears when the
+ // process is killed
+ mDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(),
+ TestDatabase.class)
+ // allowing main thread queries, just for testing
+ .allowMainThreadQueries()
+ .build();
+ }
+
+ @Test
+ public void testFlowableInTransaction() {
+ // When subscribing to the emissions of the user
+ TestSubscriber<User> subscriber = mDatabase.getUserDao().flowableUserById(3).test();
+ subscriber.assertValueCount(0);
+
+ // When inserting a new user in the data source
+ mDatabase.beginTransaction();
+ try {
+ mDatabase.getUserDao().insert(TestUtil.createUser(3));
+ mDatabase.setTransactionSuccessful();
+ } finally {
+ mDatabase.endTransaction();
+ }
+
+ subscriber.assertValueCount(1);
+ }
+}
diff --git a/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java b/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
index 8861adb..f8049f3 100644
--- a/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
@@ -502,4 +502,26 @@
assertThat(mUserDao.updateByAgeAndIds(3f, 30, Arrays.asList(3, 5)), is(1));
assertThat(mUserDao.loadByIds(3)[0].getWeight(), is(3f));
}
+
+ @Test
+ public void transactionByAnnotation() {
+ User a = TestUtil.createUser(3);
+ User b = TestUtil.createUser(5);
+ mUserDao.insertBothByAnnotation(a, b);
+ assertThat(mUserDao.count(), is(2));
+ }
+
+ @Test
+ public void transactionByAnnotation_failure() {
+ User a = TestUtil.createUser(3);
+ User b = TestUtil.createUser(3);
+ boolean caught = false;
+ try {
+ mUserDao.insertBothByAnnotation(a, b);
+ } catch (SQLiteConstraintException e) {
+ caught = true;
+ }
+ assertTrue("SQLiteConstraintException expected", caught);
+ assertThat(mUserDao.count(), is(0));
+ }
}
diff --git a/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java b/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java
index 51d5bb3..ec77561 100644
--- a/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java
@@ -18,6 +18,7 @@
import android.arch.persistence.room.Room;
import android.arch.persistence.room.integration.testapp.TestDatabase;
+import android.arch.persistence.room.integration.testapp.dao.FunnyNamedDao;
import android.arch.persistence.room.integration.testapp.dao.PetCoupleDao;
import android.arch.persistence.room.integration.testapp.dao.PetDao;
import android.arch.persistence.room.integration.testapp.dao.SchoolDao;
@@ -42,6 +43,7 @@
protected ToyDao mToyDao;
protected SpecificDogDao mSpecificDogDao;
protected WithClauseDao mWithClauseDao;
+ protected FunnyNamedDao mFunnyNamedDao;
@Before
public void createDb() {
@@ -55,5 +57,6 @@
mToyDao = mDatabase.getToyDao();
mSpecificDogDao = mDatabase.getSpecificDogDao();
mWithClauseDao = mDatabase.getWithClauseDao();
+ mFunnyNamedDao = mDatabase.getFunnyNamedDao();
}
}
diff --git a/android/arch/persistence/room/integration/testapp/test/WithClauseTest.java b/android/arch/persistence/room/integration/testapp/test/WithClauseTest.java
index 10897da..9209638 100644
--- a/android/arch/persistence/room/integration/testapp/test/WithClauseTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/WithClauseTest.java
@@ -20,6 +20,8 @@
import static org.hamcrest.MatcherAssert.assertThat;
import android.arch.persistence.room.integration.testapp.vo.User;
+import android.os.Build;
+import android.support.test.filters.SdkSuppress;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
@@ -32,6 +34,7 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
public class WithClauseTest extends TestDatabaseTest{
@Test
public void noSourceOfData() {
diff --git a/android/arch/persistence/room/integration/testapp/vo/FunnyNamedEntity.java b/android/arch/persistence/room/integration/testapp/vo/FunnyNamedEntity.java
new file mode 100644
index 0000000..20f3c21
--- /dev/null
+++ b/android/arch/persistence/room/integration/testapp/vo/FunnyNamedEntity.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.integration.testapp.vo;
+
+import android.arch.persistence.room.ColumnInfo;
+import android.arch.persistence.room.Entity;
+import android.arch.persistence.room.PrimaryKey;
+
+/**
+ * An entity that was weird names
+ */
+@Entity(tableName = FunnyNamedEntity.TABLE_NAME)
+public class FunnyNamedEntity {
+ public static final String TABLE_NAME = "funny but not so funny";
+ public static final String COLUMN_ID = "_this $is id$";
+ public static final String COLUMN_VALUE = "unlikely-Ωşå¨ıünames";
+ @PrimaryKey(autoGenerate = true)
+ @ColumnInfo(name = COLUMN_ID)
+ private int mId;
+ @ColumnInfo(name = COLUMN_VALUE)
+ private String mValue;
+
+ public FunnyNamedEntity(int id, String value) {
+ mId = id;
+ mValue = value;
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ public void setId(int id) {
+ mId = id;
+ }
+
+ public String getValue() {
+ return mValue;
+ }
+
+ public void setValue(String value) {
+ mValue = value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ FunnyNamedEntity entity = (FunnyNamedEntity) o;
+
+ if (mId != entity.mId) return false;
+ return mValue != null ? mValue.equals(entity.mValue) : entity.mValue == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mId;
+ result = 31 * result + (mValue != null ? mValue.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/android/arch/persistence/room/integration/testapp/vo/IntegerPKeyEntity.java b/android/arch/persistence/room/integration/testapp/vo/IntegerPKeyEntity.java
new file mode 100644
index 0000000..cae1843
--- /dev/null
+++ b/android/arch/persistence/room/integration/testapp/vo/IntegerPKeyEntity.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.integration.testapp.vo;
+
+import android.arch.persistence.room.Entity;
+import android.arch.persistence.room.PrimaryKey;
+
+@Entity
+public class IntegerPKeyEntity {
+ @PrimaryKey
+ public Integer pKey;
+ public String data;
+}
diff --git a/android/arch/persistence/room/integration/testapp/vo/ObjectPKeyEntity.java b/android/arch/persistence/room/integration/testapp/vo/ObjectPKeyEntity.java
new file mode 100644
index 0000000..895a35a
--- /dev/null
+++ b/android/arch/persistence/room/integration/testapp/vo/ObjectPKeyEntity.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.integration.testapp.vo;
+
+import android.arch.persistence.room.Entity;
+import android.arch.persistence.room.PrimaryKey;
+import android.support.annotation.NonNull;
+
+@Entity
+public class ObjectPKeyEntity {
+ @PrimaryKey
+ @NonNull
+ public String pKey;
+ public String data;
+
+ public ObjectPKeyEntity(String pKey, String data) {
+ this.pKey = pKey;
+ this.data = data;
+ }
+}
diff --git a/android/arch/persistence/room/integration/testapp/vo/PetCouple.java b/android/arch/persistence/room/integration/testapp/vo/PetCouple.java
index f27b131..e5208ed 100644
--- a/android/arch/persistence/room/integration/testapp/vo/PetCouple.java
+++ b/android/arch/persistence/room/integration/testapp/vo/PetCouple.java
@@ -20,11 +20,13 @@
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.PrimaryKey;
import android.arch.persistence.room.RoomWarnings;
+import android.support.annotation.NonNull;
@Entity
@SuppressWarnings(RoomWarnings.PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED)
public class PetCouple {
@PrimaryKey
+ @NonNull
public String id;
@Embedded(prefix = "male_")
public Pet male;
diff --git a/android/arch/persistence/room/migration/TableInfoTest.java b/android/arch/persistence/room/migration/TableInfoTest.java
index c6eade5..d88c02f 100644
--- a/android/arch/persistence/room/migration/TableInfoTest.java
+++ b/android/arch/persistence/room/migration/TableInfoTest.java
@@ -37,6 +37,7 @@
import org.junit.runner.RunWith;
import java.io.IOException;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -179,6 +180,35 @@
Collections.<TableInfo.ForeignKey>emptySet())));
}
+ @Test
+ public void readIndices() {
+ mDb = createDatabase(
+ "CREATE TABLE foo (n INTEGER, indexed TEXT, unique_indexed TEXT,"
+ + "a INTEGER, b INTEGER);",
+ "CREATE INDEX foo_indexed ON foo(indexed);",
+ "CREATE UNIQUE INDEX foo_unique_indexed ON foo(unique_indexed COLLATE NOCASE"
+ + " DESC);",
+ "CREATE INDEX " + TableInfo.Index.DEFAULT_PREFIX + "foo_composite_indexed"
+ + " ON foo(a, b);"
+ );
+ TableInfo info = TableInfo.read(mDb, "foo");
+ assertThat(info, is(new TableInfo(
+ "foo",
+ toMap(new TableInfo.Column("n", "INTEGER", false, 0),
+ new TableInfo.Column("indexed", "TEXT", false, 0),
+ new TableInfo.Column("unique_indexed", "TEXT", false, 0),
+ new TableInfo.Column("a", "INTEGER", false, 0),
+ new TableInfo.Column("b", "INTEGER", false, 0)),
+ Collections.<TableInfo.ForeignKey>emptySet(),
+ toSet(new TableInfo.Index("index_foo_blahblah", false,
+ Arrays.asList("a", "b")),
+ new TableInfo.Index("foo_unique_indexed", true,
+ Arrays.asList("unique_indexed")),
+ new TableInfo.Index("foo_indexed", false,
+ Arrays.asList("indexed"))))
+ ));
+ }
+
private static Map<String, TableInfo.Column> toMap(TableInfo.Column... columns) {
Map<String, TableInfo.Column> result = new HashMap<>();
for (TableInfo.Column column : columns) {
@@ -187,6 +217,14 @@
return result;
}
+ private static <T> Set<T> toSet(T... ts) {
+ final HashSet<T> result = new HashSet<T>();
+ for (T t : ts) {
+ result.add(t);
+ }
+ return result;
+ }
+
@After
public void closeDb() throws IOException {
if (mDb != null && mDb.isOpen()) {
@@ -199,8 +237,7 @@
SupportSQLiteOpenHelper.Configuration
.builder(InstrumentationRegistry.getTargetContext())
.name(null)
- .version(1)
- .callback(new SupportSQLiteOpenHelper.Callback() {
+ .callback(new SupportSQLiteOpenHelper.Callback(1) {
@Override
public void onCreate(SupportSQLiteDatabase db) {
for (String query : queries) {
diff --git a/android/arch/persistence/room/package-info.java b/android/arch/persistence/room/package-info.java
index faaa952..1dafc1b 100644
--- a/android/arch/persistence/room/package-info.java
+++ b/android/arch/persistence/room/package-info.java
@@ -39,8 +39,8 @@
* database row. For each {@link android.arch.persistence.room.Entity Entity}, a database table
* is created to hold the items. The Entity class must be referenced in the
* {@link android.arch.persistence.room.Database#entities() Database#entities} array. Each field
- * of the Entity is persisted in the database unless it is annotated with
- * {@link android.arch.persistence.room.Ignore Ignore}. Entities must have no-arg constructors.
+ * of the Entity (and its super class) is persisted in the database unless it is denoted
+ * otherwise (see {@link android.arch.persistence.room.Entity Entity} docs for details).
* </li>
* <li>{@link android.arch.persistence.room.Dao Dao}: This annotation marks a class or interface
* as a Data Access Object. Data access objects are the main component of Room that are
diff --git a/android/arch/persistence/room/testing/MigrationTestHelper.java b/android/arch/persistence/room/testing/MigrationTestHelper.java
index aea3e96..18e0a14 100644
--- a/android/arch/persistence/room/testing/MigrationTestHelper.java
+++ b/android/arch/persistence/room/testing/MigrationTestHelper.java
@@ -29,6 +29,7 @@
import android.arch.persistence.room.migration.bundle.EntityBundle;
import android.arch.persistence.room.migration.bundle.FieldBundle;
import android.arch.persistence.room.migration.bundle.ForeignKeyBundle;
+import android.arch.persistence.room.migration.bundle.IndexBundle;
import android.arch.persistence.room.migration.bundle.SchemaBundle;
import android.arch.persistence.room.util.TableInfo;
import android.content.Context;
@@ -146,7 +147,7 @@
RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration,
new CreatingDelegate(schemaBundle.getDatabase()),
schemaBundle.getDatabase().getIdentityHash());
- return openDatabase(name, version, roomOpenHelper);
+ return openDatabase(name, roomOpenHelper);
}
/**
@@ -189,17 +190,15 @@
RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration,
new MigratingDelegate(schemaBundle.getDatabase(), validateDroppedTables),
schemaBundle.getDatabase().getIdentityHash());
- return openDatabase(name, version, roomOpenHelper);
+ return openDatabase(name, roomOpenHelper);
}
- private SupportSQLiteDatabase openDatabase(String name, int version,
- RoomOpenHelper roomOpenHelper) {
+ private SupportSQLiteDatabase openDatabase(String name, RoomOpenHelper roomOpenHelper) {
SupportSQLiteOpenHelper.Configuration config =
SupportSQLiteOpenHelper.Configuration
.builder(mInstrumentation.getTargetContext())
.callback(roomOpenHelper)
.name(name)
- .version(version)
.build();
SupportSQLiteDatabase db = mOpenFactory.create(config).getWritableDatabase();
mManagedDatabases.add(new WeakReference<>(db));
@@ -287,7 +286,19 @@
private static TableInfo toTableInfo(EntityBundle entityBundle) {
return new TableInfo(entityBundle.getTableName(), toColumnMap(entityBundle),
- toForeignKeys(entityBundle.getForeignKeys()));
+ toForeignKeys(entityBundle.getForeignKeys()), toIndices(entityBundle.getIndices()));
+ }
+
+ private static Set<TableInfo.Index> toIndices(List<IndexBundle> indices) {
+ if (indices == null) {
+ return Collections.emptySet();
+ }
+ Set<TableInfo.Index> result = new HashSet<>();
+ for (IndexBundle bundle : indices) {
+ result.add(new TableInfo.Index(bundle.getName(), bundle.isUnique(),
+ bundle.getColumnNames()));
+ }
+ return result;
}
private static Set<TableInfo.ForeignKey> toForeignKeys(
@@ -401,6 +412,7 @@
final DatabaseBundle mDatabaseBundle;
RoomOpenHelperDelegate(DatabaseBundle databaseBundle) {
+ super(databaseBundle.getVersion());
mDatabaseBundle = databaseBundle;
}
diff --git a/android/arch/persistence/room/util/TableInfo.java b/android/arch/persistence/room/util/TableInfo.java
index bcd2e9e..a115147 100644
--- a/android/arch/persistence/room/util/TableInfo.java
+++ b/android/arch/persistence/room/util/TableInfo.java
@@ -20,6 +20,7 @@
import android.database.Cursor;
import android.os.Build;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import java.util.ArrayList;
@@ -29,6 +30,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.TreeMap;
/**
* A data class that holds the information about a table.
@@ -56,11 +58,70 @@
public final Set<ForeignKey> foreignKeys;
+ /**
+ * Sometimes, Index information is not available (older versions). If so, we skip their
+ * verification.
+ */
+ @Nullable
+ public final Set<Index> indices;
+
@SuppressWarnings("unused")
- public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys) {
+ public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys,
+ Set<Index> indices) {
this.name = name;
this.columns = Collections.unmodifiableMap(columns);
this.foreignKeys = Collections.unmodifiableSet(foreignKeys);
+ this.indices = indices == null ? null : Collections.unmodifiableSet(indices);
+ }
+
+ /**
+ * For backward compatibility with dbs created with older versions.
+ */
+ @SuppressWarnings("unused")
+ public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys) {
+ this(name, columns, foreignKeys, Collections.<Index>emptySet());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ TableInfo tableInfo = (TableInfo) o;
+
+ if (name != null ? !name.equals(tableInfo.name) : tableInfo.name != null) return false;
+ if (columns != null ? !columns.equals(tableInfo.columns) : tableInfo.columns != null) {
+ return false;
+ }
+ if (foreignKeys != null ? !foreignKeys.equals(tableInfo.foreignKeys)
+ : tableInfo.foreignKeys != null) {
+ return false;
+ }
+ if (indices == null || tableInfo.indices == null) {
+ // if one us is missing index information, seems like we couldn't acquire the
+ // information so we better skip.
+ return true;
+ }
+ return indices.equals(tableInfo.indices);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = name != null ? name.hashCode() : 0;
+ result = 31 * result + (columns != null ? columns.hashCode() : 0);
+ result = 31 * result + (foreignKeys != null ? foreignKeys.hashCode() : 0);
+ // skip index, it is not reliable for comparison.
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "TableInfo{"
+ + "name='" + name + '\''
+ + ", columns=" + columns
+ + ", foreignKeys=" + foreignKeys
+ + ", indices=" + indices
+ + '}';
}
/**
@@ -74,7 +135,8 @@
public static TableInfo read(SupportSQLiteDatabase database, String tableName) {
Map<String, Column> columns = readColumns(database, tableName);
Set<ForeignKey> foreignKeys = readForeignKeys(database, tableName);
- return new TableInfo(tableName, columns, foreignKeys);
+ Set<Index> indices = readIndices(database, tableName);
+ return new TableInfo(tableName, columns, foreignKeys, indices);
}
private static Set<ForeignKey> readForeignKeys(SupportSQLiteDatabase database,
@@ -167,34 +229,74 @@
return columns;
}
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- TableInfo tableInfo = (TableInfo) o;
-
- if (!name.equals(tableInfo.name)) return false;
- //noinspection SimplifiableIfStatement
- if (!columns.equals(tableInfo.columns)) return false;
- return foreignKeys.equals(tableInfo.foreignKeys);
+ /**
+ * @return null if we cannot read the indices due to older sqlite implementations.
+ */
+ @Nullable
+ private static Set<Index> readIndices(SupportSQLiteDatabase database, String tableName) {
+ Cursor cursor = database.query("PRAGMA index_list(`" + tableName + "`)");
+ try {
+ final int nameColumnIndex = cursor.getColumnIndex("name");
+ final int originColumnIndex = cursor.getColumnIndex("origin");
+ final int uniqueIndex = cursor.getColumnIndex("unique");
+ if (nameColumnIndex == -1 || originColumnIndex == -1 || uniqueIndex == -1) {
+ // we cannot read them so better not validate any index.
+ return null;
+ }
+ HashSet<Index> indices = new HashSet<>();
+ while (cursor.moveToNext()) {
+ String origin = cursor.getString(originColumnIndex);
+ if (!"c".equals(origin)) {
+ // Ignore auto-created indices
+ continue;
+ }
+ String name = cursor.getString(nameColumnIndex);
+ boolean unique = cursor.getInt(uniqueIndex) == 1;
+ Index index = readIndex(database, name, unique);
+ if (index == null) {
+ // we cannot read it properly so better not read it
+ return null;
+ }
+ indices.add(index);
+ }
+ return indices;
+ } finally {
+ cursor.close();
+ }
}
- @Override
- public int hashCode() {
- int result = name.hashCode();
- result = 31 * result + columns.hashCode();
- result = 31 * result + foreignKeys.hashCode();
- return result;
- }
+ /**
+ * @return null if we cannot read the index due to older sqlite implementations.
+ */
+ @Nullable
+ private static Index readIndex(SupportSQLiteDatabase database, String name, boolean unique) {
+ Cursor cursor = database.query("PRAGMA index_xinfo(`" + name + "`)");
+ try {
+ final int seqnoColumnIndex = cursor.getColumnIndex("seqno");
+ final int cidColumnIndex = cursor.getColumnIndex("cid");
+ final int nameColumnIndex = cursor.getColumnIndex("name");
+ if (seqnoColumnIndex == -1 || cidColumnIndex == -1 || nameColumnIndex == -1) {
+ // we cannot read them so better not validate any index.
+ return null;
+ }
+ final TreeMap<Integer, String> results = new TreeMap<>();
- @Override
- public String toString() {
- return "TableInfo{"
- + "name='" + name + '\''
- + ", columns=" + columns
- + ", foreignKeys=" + foreignKeys
- + '}';
+ while (cursor.moveToNext()) {
+ int cid = cursor.getInt(cidColumnIndex);
+ if (cid < 0) {
+ // Ignore SQLite row ID
+ continue;
+ }
+ int seq = cursor.getInt(seqnoColumnIndex);
+ String columnName = cursor.getString(nameColumnIndex);
+ results.put(seq, columnName);
+ }
+ final List<String> columns = new ArrayList<>(results.size());
+ columns.addAll(results.values());
+ return new Index(name, unique, columns);
+ } finally {
+ cursor.close();
+ }
}
/**
@@ -379,4 +481,65 @@
}
}
}
+
+ /**
+ * Holds the information about an SQLite index
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static class Index {
+ // should match the value in Index.kt
+ public static final String DEFAULT_PREFIX = "index_";
+ public final String name;
+ public final boolean unique;
+ public final List<String> columns;
+
+ public Index(String name, boolean unique, List<String> columns) {
+ this.name = name;
+ this.unique = unique;
+ this.columns = columns;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Index index = (Index) o;
+ if (unique != index.unique) {
+ return false;
+ }
+ if (!columns.equals(index.columns)) {
+ return false;
+ }
+ if (name.startsWith(Index.DEFAULT_PREFIX)) {
+ return index.name.startsWith(Index.DEFAULT_PREFIX);
+ } else {
+ return name.equals(index.name);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ int result;
+ if (name.startsWith(DEFAULT_PREFIX)) {
+ result = DEFAULT_PREFIX.hashCode();
+ } else {
+ result = name.hashCode();
+ }
+ result = 31 * result + (unique ? 1 : 0);
+ result = 31 * result + columns.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Index{"
+ + "name='" + name + '\''
+ + ", unique=" + unique
+ + ", columns=" + columns
+ + '}';
+ }
+ }
}